Files
UnrealEngine/Engine/Source/Runtime/Landscape/Private/LandscapeTextureStorageProvider.cpp
2025-05-18 13:04:45 +08:00

1189 lines
43 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
LandscapeTextureStorageProvider.cpp: Alternative Texture Storage for Landscape Textures
=============================================================================*/
#include "LandscapeTextureStorageProvider.h"
#include "LandscapeDataAccess.h"
#include "RHIGlobals.h"
#include "ContentStreaming.h"
#include "LandscapeGroup.h"
#include "LandscapePrivate.h"
#include "ProfilingDebugging/IoStoreTrace.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LandscapeTextureStorageProvider)
// enables verbose debug spew
//#define ENABLE_LANDSCAPE_PROVIDER_DEBUG_SPEW 1
#ifdef ENABLE_LANDSCAPE_PROVIDER_DEBUG_SPEW
#define PROVIDER_DEBUG_LOG(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__)
#define PROVIDER_DEBUG_LOG_DETAIL(...) UE_LOG(LogLandscape, Warning, __VA_ARGS__)
#else
#define PROVIDER_DEBUG_LOG(...) UE_LOG(LogLandscape, Verbose, __VA_ARGS__)
#define PROVIDER_DEBUG_LOG_DETAIL(...) do {} while(0)
#endif // ENABLE_LANDSCAPE_PROVIDER_DEBUG_SPEW
FLandscapeTextureMipEdgeOverrideProvider::FLandscapeTextureMipEdgeOverrideProvider(ULandscapeHeightmapTextureEdgeFixup* InEdgeFixup, UTexture2D* InTexture)
: FTextureMipDataProvider(InTexture, ETickState::GetMips, ETickThread::Async)
{
this->EdgeFixup = InEdgeFixup;
TextureName = InTexture->GetFName();
}
FLandscapeTextureMipEdgeOverrideProvider::~FLandscapeTextureMipEdgeOverrideProvider()
{
}
void FLandscapeTextureMipEdgeOverrideProvider::Init(const FTextureUpdateContext& Context, const FTextureUpdateSyncOptions& SyncOptions)
{
AdvanceTo(ETickState::GetMips, ETickThread::Async);
}
int32 FLandscapeTextureMipEdgeOverrideProvider::GetMips(const FTextureUpdateContext& Context, int32 StartingMipIndex, const FTextureMipInfoArray& MipInfos, const FTextureUpdateSyncOptions& SyncOptions)
{
// make a copy of the dest mip infos, for reference in PollMips
DestMipInfos = MipInfos;
AdvanceTo(ETickState::PollMips, ETickThread::Async);
return StartingMipIndex; // we don't directly handle any mips -- return the same starting mip index
}
bool FLandscapeTextureMipEdgeOverrideProvider::PollMips(const FTextureUpdateSyncOptions& SyncOptions)
{
using namespace UE::Landscape;
// poll mips will run once all io requests are complete (or cancelled)
// here we are relying on the behavior of the default providers, whose PollMips run _after_ PollMips on custom providers like this one.
// We rely on the fact that they do not modify the MipData in PollMips.
// THIS IS NOT TRUE OF ALL PROVIDERS -- for example the FLandscapeTextureStorageMipProvider will write to mip data in PollMips
// however, we handle that case by merging the override functionality into FLandscapeTextureStorageMipProvider, so we don't need a separate override provider.
bool bSuccess= true;
if (!ShouldPatchStreamingMipEdges())
{
AdvanceTo(ETickState::Done, ETickThread::None);
return bSuccess;
}
if ((EdgeFixup == nullptr) || !EdgeFixup->IsActive())
{
// this heightmap is not yet registered and active -- we can't patch yet.
// not to worry though! When it DOES register, it will fix all existing mips.
// (so this mip will be handled at that point)
PROVIDER_DEBUG_LOG_DETAIL(TEXT("---- PollMips Coord Mips (%d ... %d) -- NOT READY"), PendingFirstLODIdx, CurrentFirstLODIdx - 1);
AdvanceTo(ETickState::Done, ETickThread::None);
return bSuccess;
}
int32 PatchedEdges = 0;
// ensure no one modifies neighbor mapping or snapshots while we are reading them
FReadScopeLock ScopeReadLock(EdgeFixup->ActiveGroup->RWLock);
// Grab neighbor snapshots (null if they don't exist) -- IN A THREAD SAFE MANNER
FNeighborSnapshots NeighborSnapshots;
EdgeFixup->GetNeighborSnapshots(NeighborSnapshots);
// patch edges for ALL mips that are requested
if (NeighborSnapshots.ExistingNeighbors != ENeighborFlags::None)
{
PatchedEdges += EdgeFixup->PatchTextureEdgesForStreamingMips(PendingFirstLODIdx, CurrentFirstLODIdx, DestMipInfos, NeighborSnapshots);
}
PROVIDER_DEBUG_LOG(TEXT("---- PollMips Coord (%d,%d) Mips (%d ... %d) -- PATCHED %d edges"), EdgeFixup->GetGroupCoord().X, EdgeFixup->GetGroupCoord().Y, PendingFirstLODIdx, CurrentFirstLODIdx - 1, PatchedEdges);
AdvanceTo(ETickState::Done, ETickThread::None);
return bSuccess;
}
void FLandscapeTextureMipEdgeOverrideProvider::CleanUp(const FTextureUpdateSyncOptions& SyncOptions)
{
AdvanceTo(ETickState::Done, ETickThread::None);
}
void FLandscapeTextureMipEdgeOverrideProvider::Cancel(const FTextureUpdateSyncOptions& SyncOptions)
{
}
FTextureMipDataProvider::ETickThread FLandscapeTextureMipEdgeOverrideProvider::GetCancelThread() const
{
return ETickThread::None;
}
ULandscapeTextureMipEdgeOverrideFactory::ULandscapeTextureMipEdgeOverrideFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
ULandscapeTextureMipEdgeOverrideFactory* ULandscapeTextureMipEdgeOverrideFactory::AddTo(UTexture2D* TargetTexture)
{
check(TargetTexture);
// try to get an existing factory
ULandscapeTextureMipEdgeOverrideFactory* Factory = TargetTexture->GetAssetUserData<ULandscapeTextureMipEdgeOverrideFactory>();
if (Factory == nullptr)
{
// create a new one (with TargetTexture as outer)
Factory = NewObject<ULandscapeTextureMipEdgeOverrideFactory>(TargetTexture);
Factory->Texture = TargetTexture;
TargetTexture->AddAssetUserData(Factory);
}
check(Factory->Texture == TargetTexture);
check(Factory->GetOuter() == TargetTexture);
return Factory;
}
void ULandscapeTextureMipEdgeOverrideFactory::SetupEdgeFixup(ULandscapeHeightmapTextureEdgeFixup* InEdgeFixup)
{
this->EdgeFixup = InEdgeFixup;
}
void ULandscapeTextureMipEdgeOverrideFactory::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
Ar << Texture;
}
void ULandscapeTextureMipEdgeOverrideFactory::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
Super::AddReferencedObjects(InThis, Collector);
ULandscapeTextureMipEdgeOverrideFactory* const TypedThis = Cast<ULandscapeTextureMipEdgeOverrideFactory>(InThis);
Collector.AddReferencedObject(TypedThis->Texture);
Collector.AddReferencedObject(TypedThis->EdgeFixup);
}
FLandscapeTextureStorageMipProvider::FLandscapeTextureStorageMipProvider(ULandscapeTextureStorageProviderFactory* InFactory)
: FTextureMipDataProvider(InFactory->Texture, ETickState::Init, ETickThread::Async)
{
this->Factory = InFactory;
TextureName = InFactory->Texture->GetFName();
}
FLandscapeTextureStorageMipProvider::~FLandscapeTextureStorageMipProvider()
{
}
ULandscapeTextureStorageProviderFactory::ULandscapeTextureStorageProviderFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void FLandscapeTexture2DMipMap::Serialize(FArchive& Ar, UObject* Owner, uint32 SaveOverrideFlags)
{
Ar << SizeX;
Ar << SizeY;
Ar << bCompressed;
BulkData.SerializeWithFlags(Ar, Owner, SaveOverrideFlags);
}
template<typename T, typename F>
static bool SerializeArray(FArchive& Ar, TArray<T>& Array, F&& SerializeElementFn)
{
int32 Num = Array.Num();
Ar << Num;
if (Ar.IsLoading())
{
if (Num < 0)
{
return false;
}
else
{
Array.SetNum(Num);
for (int32 Index = 0; Index < Num; ++Index)
{
SerializeElementFn(Ar, Index, Array[Index]);
}
}
}
else
{
for (int32 Index = 0; Index < Num; ++Index)
{
SerializeElementFn(Ar, Index, Array[Index]);
}
}
return true;
}
void ULandscapeTextureStorageProviderFactory::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
// mip 0 mip N
// high rez <---------------------------------------------> low rez
// [ Optional Mips ][ Non Optional Mips ]
// [ Streaming Mips ][ Non Streaming Inline Mips ]
int32 OptionalMips = Mips.Num() - NumNonOptionalMips;
check(OptionalMips >= 0);
int32 FirstInlineMip = Mips.Num() - NumNonStreamingMips;
check(FirstInlineMip >= 0);
Ar << NumNonOptionalMips;
Ar << NumNonStreamingMips;
Ar << LandscapeGridScale;
SerializeArray(Ar, Mips,
[this, OptionalMips, FirstInlineMip](FArchive& Ar, int32 Index, FLandscapeTexture2DMipMap& Mip)
{
// select bulk data flags for optional/streaming/inline mips
uint32 BulkDataFlags;
if (Index < OptionalMips)
{
// optional mip
BulkDataFlags = BULKDATA_Force_NOT_InlinePayload | BULKDATA_OptionalPayload;
}
else if (Index < FirstInlineMip)
{
// streaming mip
bool bDuplicateNonOptionalMips = false; // TODO [chris.tchou] : if we add support for optional mips, we might need to calculate this.
BulkDataFlags = BULKDATA_Force_NOT_InlinePayload | (bDuplicateNonOptionalMips ? BULKDATA_DuplicateNonOptionalPayload : 0);
}
else
{
// non streaming inline mip (can be single use as we only need to upload to GPU once, are never streamed out)
BulkDataFlags = BULKDATA_ForceInlinePayload | BULKDATA_SingleUse;
}
Mip.Serialize(Ar, this, BulkDataFlags);
});
Ar << Texture;
}
void ULandscapeTextureStorageProviderFactory::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
Super::AddReferencedObjects(InThis, Collector);
ULandscapeTextureStorageProviderFactory* const TypedThis = Cast<ULandscapeTextureStorageProviderFactory>(InThis);
Collector.AddReferencedObject(TypedThis->Texture);
Collector.AddReferencedObject(TypedThis->EdgeFixup);
}
FStreamableRenderResourceState ULandscapeTextureStorageProviderFactory::GetResourcePostInitState(const UTexture* Owner, bool bAllowStreaming)
{
// We are using the non-offline mode to upload these textures currently, so we don't need to consider mip tails.
// (RHI will handle it during upload, just less optimal than having them pre-packed)
// If we ever want to optimize the GPU upload by using the offline mode, we can add the logic here to take mip tails into account.
const int32 PlatformNumMipsInTail = 1;
const int32 TotalMips = Mips.Num();
const int32 ExpectedAssetLODBias = FMath::Clamp<int32>(Owner->GetCachedLODBias() - Owner->NumCinematicMipLevels, 0, TotalMips - 1);
const int32 MaxRuntimeMipCount = FMath::Min<int32>(GMaxTextureMipCount, FStreamableRenderResourceState::MAX_LOD_COUNT);
const int32 NumMips = FMath::Min<int32>(TotalMips - ExpectedAssetLODBias, MaxRuntimeMipCount);
bool bTextureIsStreamable = true; // landscape texture storage is always streamable (we should not use it for platforms that are not)
// clamp non-optional and non-streaming mips to reflect potentially reduced mip count because of bias
const int32 BiasedNumNonOptionalMips = FMath::Min<int32>(NumMips, NumNonOptionalMips);
const int32 NumOfNonStreamingMips = FMath::Min<int32>(NumMips, NumNonStreamingMips);
// Optional mips must be streaming mips :
check(BiasedNumNonOptionalMips >= NumOfNonStreamingMips);
if (NumOfNonStreamingMips == NumMips)
{
bTextureIsStreamable = false;
}
const int32 AssetMipIdxForResourceFirstMip = FMath::Max<int32>(0, TotalMips - NumMips);
const bool bMakeStreamable = bAllowStreaming;
int32 NumRequestedMips = 0;
if (!bTextureIsStreamable)
{
// in Editor , NumOfNonStreamingMips may not be all mips
// but once we cook it will be
// so check this early to make behavior consistent
NumRequestedMips = NumMips;
}
else if (bMakeStreamable && IStreamingManager::Get().IsRenderAssetStreamingEnabled(EStreamableRenderAssetType::Texture))
{
NumRequestedMips = NumOfNonStreamingMips;
}
else
{
// we are not streaming (bMakeStreamable is false)
// but this may select a mip below the top mip
// (due to cinematic lod bias)
// but only if the texture itself is streamable
// Adjust CachedLODBias so that it takes into account FStreamableRenderResourceState::AssetLODBias.
const int32 ResourceLODBias = FMath::Max<int32>(0, Owner->GetCachedLODBias() - AssetMipIdxForResourceFirstMip);
// Ensure NumMipsInTail is within valid range to safeguard on the above expressions.
const int32 NumMipsInTail = FMath::Clamp<int32>(PlatformNumMipsInTail, 1, NumMips);
// Bias is not allowed to shrink the mip count below NumMipsInTail.
NumRequestedMips = FMath::Max<int32>(NumMips - ResourceLODBias, NumMipsInTail);
// If trying to load optional mips, check if the first resource mip is available.
if (NumRequestedMips > BiasedNumNonOptionalMips && !DoesMipDataExist(AssetMipIdxForResourceFirstMip))
{
NumRequestedMips = BiasedNumNonOptionalMips;
}
// Ensure we don't request a top mip in the NonStreamingMips
NumRequestedMips = FMath::Max(NumRequestedMips, NumOfNonStreamingMips);
}
const int32 MinRequestMipCount = 0;
if (NumRequestedMips < MinRequestMipCount && MinRequestMipCount < NumMips)
{
NumRequestedMips = MinRequestMipCount;
}
FStreamableRenderResourceState PostInitState;
PostInitState.bSupportsStreaming = bMakeStreamable;
PostInitState.NumNonStreamingLODs = IntCastChecked<uint8>(NumOfNonStreamingMips);
PostInitState.NumNonOptionalLODs = IntCastChecked<uint8>(BiasedNumNonOptionalMips);
PostInitState.MaxNumLODs = IntCastChecked<uint8>(NumMips);
PostInitState.AssetLODBias = IntCastChecked<uint8>(AssetMipIdxForResourceFirstMip);
PostInitState.NumResidentLODs = IntCastChecked<uint8>(NumRequestedMips);
PostInitState.NumRequestedLODs = IntCastChecked<uint8>(NumRequestedMips);
return PostInitState;
}
bool ULandscapeTextureStorageProviderFactory::GetInitialMipData(int32 FirstMipToLoad, TArrayView<void*> OutMipData, TArrayView<int64> OutMipSize, FStringView DebugContext)
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureStorageProviderFactory::GetInitialMipData);
check(FirstMipToLoad >= 0);
int32 NumberOfMipsToLoad = OutMipData.Num();
check(NumberOfMipsToLoad > 0);
check(OutMipData.GetData());
const int32 LoadableMips = Mips.Num();
check(NumberOfMipsToLoad == LoadableMips - FirstMipToLoad);
const int32 MipLoadEnd = FirstMipToLoad + NumberOfMipsToLoad;
check(MipLoadEnd <= LoadableMips);
check(OutMipSize.Num() == NumberOfMipsToLoad || OutMipSize.Num() == 0);
int32 NumMipsCached = 0;
// Handle the case where we inlined more mips than we intend to upload immediately, by discarding the unneeded mips
for (int32 MipIndex = 0; MipIndex < FirstMipToLoad && MipIndex < LoadableMips; ++MipIndex)
{
FLandscapeTexture2DMipMap& Mip = Mips[MipIndex];
if (Mip.BulkData.IsBulkDataLoaded())
{
// we know inline mips are set up with the discard after first use flag, so simply locking then unlocking will cause them to be deleted
Mip.BulkData.Lock(LOCK_READ_ONLY);
Mip.BulkData.Unlock();
}
}
// Get data for the remaining mips from bulk data.
for (int32 MipIndex = FirstMipToLoad; MipIndex < MipLoadEnd; ++MipIndex)
{
FLandscapeTexture2DMipMap& Mip = Mips[MipIndex];
const int64 DestBytes = Mip.SizeX * Mip.SizeY * 4;
const int64 BulkDataSize = Mip.BulkData.GetBulkDataSize();
if (BulkDataSize > 0)
{
void* SourceData = nullptr;
const bool bDiscardInternalCopy = true;
Mip.BulkData.GetCopy(&SourceData, bDiscardInternalCopy);
check(SourceData);
if (Mip.bCompressed)
{
// decompress the mip to a new buffer, then free the original buffer
uint8* DestData = static_cast<uint8*>(FMemory::Malloc(DestBytes));
DecompressMip((uint8*)SourceData, BulkDataSize, DestData, DestBytes, MipIndex);
OutMipData[MipIndex - FirstMipToLoad] = DestData;
FMemory::Free(SourceData);
}
else
{
// mip is uncompressed, it should already be the correct size, and we can just use the source data buffer directly
check(BulkDataSize == DestBytes);
OutMipData[MipIndex - FirstMipToLoad] = SourceData;
}
if (OutMipSize.Num() > 0)
{
OutMipSize[MipIndex - FirstMipToLoad] = DestBytes;
}
NumMipsCached++;
}
}
if (NumMipsCached != (LoadableMips - FirstMipToLoad))
{
UE_LOG(LogLandscape, Warning, TEXT("ULandscapeTextureStorageProviderFactory::TryLoadMips failed for %.*s, NumMipsCached: %d, LoadableMips: %d, FirstMipToLoad: %d"),
DebugContext.Len(), DebugContext.GetData(),
NumMipsCached,
LoadableMips,
FirstMipToLoad);
// Unable to cache all mips. Release memory for those that were cached.
for (int32 MipIndex = FirstMipToLoad; MipIndex < LoadableMips; ++MipIndex)
{
FLandscapeTexture2DMipMap& Mip = Mips[MipIndex];
UE_LOG(LogLandscape, Verbose, TEXT(" Mip %d, BulkDataSize: %" INT64_FMT),
MipIndex,
Mip.BulkData.GetBulkDataSize());
if (OutMipData[MipIndex - FirstMipToLoad])
{
FMemory::Free(OutMipData[MipIndex - FirstMipToLoad]);
OutMipData[MipIndex - FirstMipToLoad] = nullptr;
}
if (OutMipSize.Num() > 0)
{
OutMipSize[MipIndex - FirstMipToLoad] = 0;
}
}
return false;
}
return true;
}
#if WITH_EDITORONLY_DATA
ULandscapeTextureStorageProviderFactory* ULandscapeTextureStorageProviderFactory::ApplyTo(UTexture2D* InTargetTexture, const FVector& InLandsapeGridScale, int32 InHeightmapCompressionMipThreshold)
{
check(InTargetTexture);
check(InTargetTexture->Source.IsValid())
check(InTargetTexture->Source.GetFormat() == TSF_BGRA8);
// try to get an existing factory
ULandscapeTextureStorageProviderFactory* Factory= InTargetTexture->GetAssetUserData<ULandscapeTextureStorageProviderFactory>();
if (Factory == nullptr)
{
// create a new one
Factory = NewObject<ULandscapeTextureStorageProviderFactory>(InTargetTexture);
Factory->Texture = InTargetTexture;
InTargetTexture->AddAssetUserData(Factory);
}
Factory->UpdateCompressedDataFromSource(InTargetTexture, InLandsapeGridScale, InHeightmapCompressionMipThreshold);
return Factory;
}
void ULandscapeTextureStorageProviderFactory::UpdateCompressedDataFromSource(UTexture2D* InTargetTexture, const FVector& InLandsapeGridScale, int32 InHeightmapCompressionMipThreshold)
{
check(this->Texture == InTargetTexture);
EPixelFormat Format = PF_B8G8R8A8;
int32 Width = InTargetTexture->Source.GetSizeX();
int32 Height = InTargetTexture->Source.GetSizeY();
int32 MipCount = InTargetTexture->Source.GetNumMips();
uint32 SrcBpp = GPixelFormats[Format].BlockBytes;
uint32 SrcPitch = Width * SrcBpp;
// need this to properly calculate normals
this->LandscapeGridScale = InLandsapeGridScale;
// calculate number of non-streaming mips
// TODO [chris.tchou] : we could make this calculation platform specific, like Texture2D does.
// We would have to calculate it during serialization, when we know the target platform.
{
int32 NumberOfNonStreamingMips = 1;
// TODO [chris.tchou] : we could ensure Mip Tails are not streamed, as it's more overhead to upload.
// we would have to query TextureCompressorModule for platform specific info.
// Ignoring the mip tail should still work, just less optimal as it does more work at runtime to blit into the mip tail.
int32 NumMipsInTail = 0;
NumberOfNonStreamingMips = FMath::Max(NumberOfNonStreamingMips, NumMipsInTail);
NumberOfNonStreamingMips = FMath::Max(NumberOfNonStreamingMips, UTexture2D::GetStaticMinTextureResidentMipCount());
NumberOfNonStreamingMips = FMath::Min(NumberOfNonStreamingMips, MipCount);
this->NumNonStreamingMips = NumberOfNonStreamingMips;
}
// calculate number of non-optional mips
{
// for now, landscape texture storage does not have any optional mips
this->NumNonOptionalMips = MipCount;
}
this->Mips.Empty();
int32 MipWidth = Width;
int32 MipHeight = Height;
for (int32 MipIndex = 0; MipIndex < MipCount; MipIndex++)
{
FLandscapeTexture2DMipMap* Mip = new(this->Mips) FLandscapeTexture2DMipMap();
Mip->SizeX = MipWidth;
Mip->SizeY = MipHeight;
TArray64<uint8> MipData;
InTargetTexture->Source.GetMipData(MipData, MipIndex);
// Store mips below the threshold size uncompressed
if ((MipWidth < InHeightmapCompressionMipThreshold) || (MipHeight < InHeightmapCompressionMipThreshold))
{
Mip->bCompressed = false;
CopyMipToBulkData(MipIndex, MipWidth, MipHeight, MipData.GetData(), MipData.Num(), Mip->BulkData);
}
else
{
Mip->bCompressed = true;
CompressMipToBulkData(MipIndex, MipWidth, MipHeight, MipData.GetData(), MipData.Num(), Mip->BulkData);
}
MipWidth = FMath::Max(MipWidth / 2, 1);
MipHeight = FMath::Max(MipHeight / 2, 1);
}
}
#endif // WITH_EDITORONLY_DATA
void ULandscapeTextureStorageProviderFactory::SetupEdgeFixup(ULandscapeHeightmapTextureEdgeFixup* InEdgeFixup)
{
this->EdgeFixup = InEdgeFixup;
}
// Helper to configure the AsyncFileCallBack.
void FLandscapeTextureStorageMipProvider::CreateAsyncFileCallback(const FTextureUpdateSyncOptions& SyncOptions)
{
FThreadSafeCounter* Counter = SyncOptions.Counter;
FTextureUpdateSyncOptions::FCallback RescheduleCallback = SyncOptions.RescheduleCallback;
check(Counter && RescheduleCallback);
AsyncFileCallBack = [this, Counter, RescheduleCallback](bool bWasCancelled, IBulkDataIORequest* Req)
{
// At this point task synchronization would hold the number of pending requests.
Counter->Decrement();
if (bWasCancelled)
{
bIORequestCancelled = true;
}
if (Counter->GetValue() == 0)
{
RescheduleCallback();
}
};
}
void FLandscapeTextureStorageMipProvider::ClearIORequests()
{
for (FIORequest& IORequest : IORequests)
{
// If requests are not yet completed, cancel and wait.
if (IORequest.BulkDataIORequest && !IORequest.BulkDataIORequest->PollCompletion())
{
IORequest.BulkDataIORequest->Cancel();
IORequest.BulkDataIORequest->WaitCompletion();
}
}
IORequests.Empty();
}
void FLandscapeTextureStorageMipProvider::Init(const FTextureUpdateContext& Context, const FTextureUpdateSyncOptions& SyncOptions)
{
IORequests.AddDefaulted(CurrentFirstLODIdx);
// If this resource has optional LODs and we are streaming one of them.
if (ResourceState.NumNonOptionalLODs < ResourceState.MaxNumLODs && PendingFirstLODIdx < ResourceState.LODCountToFirstLODIdx(ResourceState.NumNonOptionalLODs))
{
// Generate the FilenameHash of each optional LOD before the first one requested, so that we can handle properly PAK unmount events.
// Note that streamer only stores the hash for the first optional mip.
for (int32 MipIdx = 0; MipIdx < PendingFirstLODIdx; ++MipIdx)
{
const FLandscapeTexture2DMipMap* SourceMip = Factory->GetMip(MipIdx);
// const FTexture2DMipMap& OwnerMip = *Context.MipsView[MipIdx];
IORequests[MipIdx].FilenameHash = SourceMip->BulkData.GetIoFilenameHash();
}
}
// Otherwise validate each streamed in mip.
for (int32 MipIdx = PendingFirstLODIdx; MipIdx < CurrentFirstLODIdx; ++MipIdx)
{
const FLandscapeTexture2DMipMap* SourceMip = Factory->GetMip(MipIdx);
if (SourceMip->BulkData.IsStoredCompressedOnDisk())
{
// Compression at the package level is no longer supported
continue;
}
else if (SourceMip->BulkData.GetBulkDataSize() <= 0)
{
// Invalid bulk data size.
continue;
}
else
{
IORequests[MipIdx].FilenameHash = SourceMip->BulkData.GetIoFilenameHash();
}
}
AdvanceTo(ETickState::GetMips, ETickThread::Async);
}
int32 FLandscapeTextureStorageMipProvider::GetMips(const FTextureUpdateContext& Context, int32 StartingMipIndex, const FTextureMipInfoArray& MipInfos, const FTextureUpdateSyncOptions& SyncOptions)
{
CreateAsyncFileCallback(SyncOptions); // this just creates it... callback has to be passed to the IO request completion to actually get called...
check(SyncOptions.Counter != nullptr);
DestMipInfos = MipInfos;
FirstRequestedMipIndex = StartingMipIndex;
while (StartingMipIndex < CurrentFirstLODIdx && MipInfos.IsValidIndex(StartingMipIndex))
{
const FTextureMipInfo& DestMip = MipInfos[StartingMipIndex];
const FLandscapeTexture2DMipMap* SourceMip = Factory->GetMip(StartingMipIndex);
if (SourceMip == nullptr || !DestMip.DestData)
{
break;
}
// Check the validity of the filename.
if (IORequests[StartingMipIndex].FilenameHash == INVALID_IO_FILENAME_HASH)
{
break;
}
// Increment the sync counter. This causes the system to not advance to the next tick, until RescheduleCallback() is called (by AsyncFileCallBack when counter reaches zero)
// If a request completes immediately, then it will call the RescheduleCallback,
// but that won't do anything because the tick would not try to acquire the lock since it is already locked.
SyncOptions.Counter->Increment();
int64 StreamDataSize = SourceMip->BulkData.GetBulkDataSize();
if (SourceMip->bCompressed)
{
// allocate a buffer to receive the streamed data
uint8* StreamData = static_cast<uint8*>(FMemory::Malloc(StreamDataSize));
TRACE_IOSTORE_METADATA_SCOPE_TAG("Landscape");
IORequests[StartingMipIndex].BulkDataIORequest.Reset(
SourceMip->BulkData.CreateStreamingRequest(
0,
StreamDataSize,
(EAsyncIOPriorityAndFlags)FMath::Clamp<int32>(AIOP_Low + (bPrioritizedIORequest ? 1 : 0), AIOP_Low, AIOP_High) | AIOP_FLAG_DONTCACHE,
&AsyncFileCallBack,
StreamData)
);
}
else
{
// If DataSize is specified (optional, may be zero), then check that the size matches expectations
if ((DestMip.DataSize != 0) && (DestMip.DataSize != StreamDataSize))
{
// LogPlayLevel: Error: UAT: [2024.11.08 - 17.09.47:903] [10] LogLandscape : Error : Unexpected data size for landscape mip 0 : expected 0 bytes(512 x 512), has 1048576 bytes(512 x 512 compressed: 0)
UE_LOG(LogLandscape, Error, TEXT("Unexpected data size for landscape mip %d : expected %" UINT64_FMT " bytes (%d x %d), has %" INT64_FMT " bytes (%d x %d compressed: %d)"),
StartingMipIndex,
DestMip.DataSize, DestMip.SizeX, DestMip.SizeY,
StreamDataSize, SourceMip->SizeX, SourceMip->SizeY, SourceMip->bCompressed
);
check(StreamDataSize == DestMip.DataSize);
}
TRACE_IOSTORE_METADATA_SCOPE_TAG("Landscape");
IORequests[StartingMipIndex].BulkDataIORequest.Reset(
SourceMip->BulkData.CreateStreamingRequest(
0,
StreamDataSize,
(EAsyncIOPriorityAndFlags)FMath::Clamp<int32>(AIOP_Low + (bPrioritizedIORequest ? 1 : 0), AIOP_Low, AIOP_High) | AIOP_FLAG_DONTCACHE,
&AsyncFileCallBack,
(uint8*) DestMip.DestData) // when not compressed, we can stream directly into the dest mip memory
);
}
// remember the dest mip data buffer (we can't fill it out now, must wait until streaming is complete)
IORequests[StartingMipIndex].DestMipData = static_cast<uint8*>(DestMip.DestData);
StartingMipIndex++;
}
AdvanceTo(ETickState::PollMips, ETickThread::Async);
return StartingMipIndex; // return the mips we handled (if this is not CurrentFirstLODIdx, it will fall back to other providers)
}
bool FLandscapeTextureStorageMipProvider::PollMips(const FTextureUpdateSyncOptions& SyncOptions)
{
using namespace UE::Landscape;
// poll mips will run once all io requests are complete (or cancelled)
// Notify that some files have possibly been unmounted / missing.
if (bIORequestCancelled && !bIORequestAborted)
{
IRenderAssetStreamingManager& StreamingManager = IStreamingManager::Get().GetRenderAssetStreamingManager();
for (FIORequest& IORequest : IORequests)
{
StreamingManager.MarkMountedStateDirty(IORequest.FilenameHash);
}
UE_LOG(LogLandscape, Warning, TEXT("[%s] FLandscapeTextureStorageMipProvider Texture stream in request failed due to IO error (Mip %d-%d)."), *TextureName.ToString(), ResourceState.AssetLODBias + PendingFirstLODIdx, ResourceState.AssetLODBias + CurrentFirstLODIdx - 1);
}
if (!bIORequestCancelled && !bIORequestAborted)
{
// decompress the mips (note that this is using the dest mip data pointer we memorized during GetMips)
for (int MipIndex = FirstRequestedMipIndex; MipIndex < CurrentFirstLODIdx; MipIndex++)
{
const FLandscapeTexture2DMipMap* SourceMip = Factory->GetMip(MipIndex);
if (SourceMip->bCompressed)
{
uint8* SourceData = IORequests[MipIndex].BulkDataIORequest->GetReadResults();
int64 DestDataBytes = SourceMip->SizeX * SourceMip->SizeY * 4;
uint8* DestData = IORequests[MipIndex].DestMipData;
Factory->DecompressMip(SourceData, SourceMip->BulkData.GetBulkDataSize(), DestData, DestDataBytes, MipIndex);
FMemory::Free(SourceData);
}
else
{
// uncompressed streams directly into the dst mip data buffer, so nothing to do here (other than a sanity check)
uint8* SourceData = IORequests[MipIndex].BulkDataIORequest->GetReadResults();
check(IORequests[MipIndex].DestMipData == SourceData);
}
}
if (ShouldPatchStreamingMipEdges())
{
// run mip patching if EdgeFixup is valid
ULandscapeHeightmapTextureEdgeFixup* EdgeFixup = Factory->EdgeFixup;
if ((EdgeFixup != nullptr) && EdgeFixup->IsActive())
{
int32 PatchedEdges = 0;
// ensure no one modifies neighbor mapping or snapshots while we are reading them
FReadScopeLock ScopeReadLock(EdgeFixup->ActiveGroup->RWLock);
// Grab neighbor snapshots (null if they don't exist)
FNeighborSnapshots NeighborSnapshots;
EdgeFixup->GetNeighborSnapshots(NeighborSnapshots);
// patch edges for ALL mips that are requested
if (NeighborSnapshots.ExistingNeighbors != ENeighborFlags::None)
{
PatchedEdges += EdgeFixup->PatchTextureEdgesForStreamingMips(PendingFirstLODIdx, CurrentFirstLODIdx, DestMipInfos, NeighborSnapshots);
}
PROVIDER_DEBUG_LOG(TEXT("---- PollMips Coord (%d,%d) Mips (%d ... %d) -- PATCHED COMPRESSED %d edges"), EdgeFixup->GetGroupCoord().X, EdgeFixup->GetGroupCoord().Y, PendingFirstLODIdx, CurrentFirstLODIdx - 1, PatchedEdges);
}
}
}
ClearIORequests();
AdvanceTo(ETickState::Done, ETickThread::None);
return !bIORequestCancelled; // return true if successful and it can upload the DestMip data to the GPU
}
void FLandscapeTextureStorageMipProvider::AbortPollMips()
{
// ... cancel all streaming ops in progress ...
for (FIORequest& IORequest : IORequests)
{
if (IORequest.BulkDataIORequest)
{
// Calling cancel() here will trigger the AsyncFileCallBack and precipitate the execution of Cancel().
IORequest.BulkDataIORequest->Cancel();
bIORequestAborted = true;
}
}
}
void FLandscapeTextureStorageMipProvider::CleanUp(const FTextureUpdateSyncOptions& SyncOptions)
{
AdvanceTo(ETickState::Done, ETickThread::None);
}
void FLandscapeTextureStorageMipProvider::Cancel(const FTextureUpdateSyncOptions& SyncOptions)
{
ClearIORequests();
}
FTextureMipDataProvider::ETickThread FLandscapeTextureStorageMipProvider::GetCancelThread() const
{
return IORequests.Num() ? FTextureMipDataProvider::ETickThread::Async : FTextureMipDataProvider::ETickThread::None;
}
void ULandscapeTextureStorageProviderFactory::CopyMipToBulkData(int32 MipIndex, int32 MipSizeX, int32 MipSizeY, uint8* SourceData, int32 SourceDataBytes, FByteBulkData& DestBulkData)
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureStorageProviderFactory::CopyMipToBulkData);
DestBulkData.Lock(LOCK_READ_WRITE);
int32 TotalPixels = MipSizeX * MipSizeY;
checkf(SourceDataBytes == TotalPixels * 4, TEXT("SourceDataBytes: %d TotalPixels: %d MipIndex: %d MipSizeX: %d MipSizeY: %d"), SourceDataBytes, TotalPixels, MipIndex, MipSizeX, MipSizeY);
int32 DestBytes = SourceDataBytes;
uint8* DestData = DestBulkData.Realloc(DestBytes);
memcpy(DestData, SourceData, DestBytes);
DestBulkData.Unlock();
}
void ULandscapeTextureStorageProviderFactory::CompressMipToBulkData(int32 MipIndex, int32 MipSizeX, int32 MipSizeY, uint8* SourceData, int32 SourceDataBytes, FByteBulkData& DestBulkData)
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureStorageProviderFactory::CompressMipToBulkData);
DestBulkData.Lock(LOCK_READ_WRITE);
int32 TotalPixels = MipSizeX * MipSizeY;
check(SourceDataBytes == TotalPixels * 4);
check(TotalPixels >= 16); // shouldn't be used on very small mips
// DestData consists of a 16 bit height per pixel, then an 8:8 normal per edge pixel
int32 DestBytes = (TotalPixels + (MipSizeX + MipSizeY) * 2 - 4) * 2;
uint8* DestData = DestBulkData.Realloc(DestBytes);
// delta encode the heights -- this (usually) greatly reduces the variance in the data, which makes it compress much better on disk when package compression is applied.
uint16 LastHeight = 32768;
int32 DestOffset = 0;
for (int32 SourceOffset = 0; SourceOffset < SourceDataBytes; SourceOffset += 4)
{
// texture data is stored as BGRA, or [normal x, height low bits, height high bits, normal y]
uint16 Height = SourceData[SourceOffset + 2] * 256 + SourceData[SourceOffset + 1];
uint16 DeltaHeight = Height - LastHeight;
LastHeight = Height;
// store delta height
DestData[DestOffset + 0] = DeltaHeight >> 8;
DestData[DestOffset + 1] = DeltaHeight & 0xff;
DestOffset += 2;
}
int32 DeltaCount = DestOffset;
// capture normals along the edge (delta encoded clockwise starting from top left)
uint8 LastNormalX = 128;
uint8 LastNormalY = 128;
auto EncodeNormal = [&LastNormalX, &LastNormalY, SourceData, DestData, MipSizeX, &DestOffset](int32 X, int32 Y)
{
int32 SourceOffset = (Y * MipSizeX + X) * 4;
uint8 NormalX = SourceData[SourceOffset + 0];
uint8 NormalY = SourceData[SourceOffset + 3];
DestData[DestOffset + 0] = NormalX - LastNormalX;
DestData[DestOffset + 1] = NormalY - LastNormalY;
LastNormalX = NormalX;
LastNormalY = NormalY;
DestOffset += 2;
};
for (int32 X = 0; X < MipSizeX; X++) // [0 ... MipSizeX-1], 0
{
EncodeNormal(X, 0);
}
for (int32 Y = 1; Y < MipSizeY; Y++) // MipSizeX-1, [1 ... MipSizeY-1]
{
EncodeNormal(MipSizeX - 1, Y);
}
for (int32 X = MipSizeX-2; X >= 0; X--) // [MipSizeX-2 ... 0], MipSizeY-1
{
EncodeNormal(X, MipSizeY - 1);
}
for (int32 Y = MipSizeY-2; Y >= 1; Y--) // 0, [MipSizeY-2 ... 1]
{
EncodeNormal(0, Y);
}
check(DestOffset == DestBytes);
DestBulkData.Unlock();
}
// Compute the normal of the triangle formed by the 3 points (in winding order).
inline FVector ComputeTriangleNormal(const FVector& InPoint0, const FVector& InPoint1, const FVector& InPoint2)
{
FVector Normal = (InPoint0 - InPoint1).Cross(InPoint1 - InPoint2);
Normal.Normalize();
return Normal;
}
// This function is unused, but explains how we get from ComputeTriangleNormal above to the optimized version below.
// When computing normals on a height grid, you can simplify the math, and only care about delta height in the +X and +Y directions.
// Then we can take advantage of the zeros in DX and DY to simplify the cross product:
inline FVector3f ComputeGridNormalFromDeltaHeights(float DHDX, float DHDY, int32 MipScale, const FVector& LandscapeGridScale)
{
// by placing the origin at the center vertex, and ensuring one vector is along +X and the other along +Y, a lot of math is removed:
// FVector3f Center(0.0f, 0.0f, 0.0f);
FVector3f DX(MipScale * LandscapeGridScale.X, 0.0, DHDX * (float)LandscapeGridScale.Z);
FVector3f DY(0.0, MipScale * LandscapeGridScale.Y, DHDY * (float)LandscapeGridScale.Z);
FVector3f Normal; // = (DX-Center).Cross(DY-Center);
{
Normal.X = /*DX.Y * DY.Z*/ - DX.Z * DY.Y; // DHDX * (-LGS.Z * LGS.Y * MipScale) << note values in parens are constant in the inner loop
Normal.Y = /*DX.Z * DY.X*/ - DX.X * DY.Z; // DHDY * (-LGS.Z * LGS.X * MipScale)
Normal.Z = DX.X * DY.Y /* - DX.Y * DY.X*/; // (LGS.X * LGS.Y * MipScale * MipScale) << fully constant in the inner loop
}
Normal.Normalize();
return Normal;
}
inline FVector2f CalculatePremultU16(int32 MipIndex, const FVector& LandscapeGridScale)
{
// We optimize the cross product calculation in the inner loop by precalculating the DHDX and DHDY multipliers.
int32 MipScale = 1 << MipIndex;
// Note that we're also doing an optimization trick by scaling the resulting vector such that CrossProductResult.Z == 1.0.
// Since we pass the result through Normalize(), that scale factor doesn't matter -- but it's faster to calculate that way.
// LANDSCAPE_ZSCALE comes from the fact that we are operating on the integer height values, and this converts the integer heights to landscape space
// (and then the LandscapeGridScale converts that to world space)
float ScaleFactor = -LANDSCAPE_ZSCALE / (LandscapeGridScale.X * LandscapeGridScale.Y * MipScale);
FVector2f PremultU16;
PremultU16.X = LandscapeGridScale.Z * LandscapeGridScale.Y * ScaleFactor;
PremultU16.Y = LandscapeGridScale.Z * LandscapeGridScale.X * ScaleFactor;
return PremultU16;
}
// This takes it a few steps further : we've minimized the math here by premultiplying everything related to LGS, MipScale, and LandscapeScale into PremultLGS
// We've also scaled up the results so that Normal.Z == 1, which reduces the math used by the Normalize.
inline FVector3f ComputeGridNormalFromDeltaHeightsPremultU16(int32 DHDX, int32 DHDY, const FVector2f& PremultU16)
{
FVector3f Normal; // = DX.Cross(DY);
{
// we've calculated PremultU16 to ensure Normal.Z is 1.0 (see CalculatePremultU16), which saves some math in Normalize()
Normal.X = DHDX * PremultU16.X;
Normal.Y = DHDY * PremultU16.Y;
// Normal.Z = 1.0f;
}
// Normal.Normalize(); optimized below
{
const float SquareSum = Normal.X * Normal.X + Normal.Y * Normal.Y + 1.0f;
if (SquareSum > UE_SMALL_NUMBER)
{
// sqrt estimate should be more than sufficient for 8 bit results.
const float Scale = FMath::InvSqrtEst(SquareSum);
Normal.X *= Scale;
Normal.Y *= Scale;
Normal.Z = Scale; // take advantage of knowing Normal.Z == 1.0
}
else
{
Normal.X = 0.0f;
Normal.Y = 0.0f;
Normal.Z = 1.0f;
}
}
return Normal;
}
inline void SampleWorldPositionAtOffset(FVector& OutPoint, const uint8* MipData, int32 X, int32 Y, int32 MipSizeX, const FVector& InLandscapeGridScale)
{
int32 OffsetBytes = (Y * MipSizeX + X) * 4;
uint16 HeightData = MipData[OffsetBytes + 2] * 256 + MipData[OffsetBytes + 1];
// NOTE: since we are using deltas between points to calculate the normal, we don't care about constant offsets in the position, only relative scales
OutPoint.Set(
X * InLandscapeGridScale.X,
Y * InLandscapeGridScale.Y,
LandscapeDataAccess::GetLocalHeight(HeightData) * InLandscapeGridScale.Z);
}
inline uint16 DecodeHeightU16(const FColor* Pixel)
{
uint16 HeightData = Pixel->R * 256 + Pixel->G;
return HeightData;
}
inline void FastNormalize(FVector3f& V)
{
const float SquareSum = V.X * V.X + V.Y * V.Y + V.Z * V.Z;
if (SquareSum > UE_SMALL_NUMBER)
{
const float Scale = FMath::InvSqrtEst(SquareSum);
V *= Scale;
}
else
{
V.X = 0.0f;
V.Y = 0.0f;
V.Z = 1.0f;
}
}
// The triangle topology is the following (where C = center, T = top, B = bottom, L = left, R = right and Nx the normals we need to interpolate):
// . ------ . --------.
// | | \ |
// \ |
// | | \ |
// P0'\ P1'| N0' | << normals calculated for the previous line
// | | \ |
// \ |
// | | \ |
// . - - - - TL ------ TT
// | | \ |
// \ | \ |
// | | \ |
// P0 \ P1 | N0 \ N1 |
// | | \ |
// \ | \ |
// | | \ |
// . - - - - LL ------ CC << current pixel being processed
//
// we calculate normals while we decompress, as a single pass gives better cache coherency.
// while iterating each interior pixel left to right, top to bottom, we:
// 1) Decode Height at CC (current pixel)
// 2) Write Height at CC
// 3) Compute N0/N1 using heights at CC/TT/TL/LL (all previously decoded)
// 4) Complete Normal calculation for TL == (P0' + P1' + N0') + P1 + N0 + N1
// 5) Write Normal for TL
// 6) Store Partial Normal for LL in PrevLine cache -- stores P0 + P1 + N0
void ULandscapeTextureStorageProviderFactory::DecompressMip(uint8* SourceData, int64 SourceDataBytes, uint8* DestData, int64 DestDataBytes, int32 MipIndex)
{
TRACE_CPUPROFILER_EVENT_SCOPE(ULandscapeTextureStorageProviderFactory::DecompressMip);
check(SourceData && DestData);
FLandscapeTexture2DMipMap& Mip = Mips[MipIndex];
check(Mip.bCompressed); // uncompressed should be handled outside of this function
int32 Width = Mip.SizeX;
int32 Height = Mip.SizeY;
int32 TotalPixels = Width * Height;
int32 BorderPixels = (Width + Height) * 2 - 4;
check(SourceDataBytes == (TotalPixels + BorderPixels) * 2); // 2 bytes (height) for each pixel, plus 2 bytes (normal x/y) for each border pixel
check(DestDataBytes == TotalPixels * 4);
// save some multiplying by premultiplying the grid scales, mip scale and ZScale
FVector2f PremultU16 = CalculatePremultU16(MipIndex, LandscapeGridScale);
// current center pixel height
// (also used to delta decode the heights - initial value must match the initial value used during encoding)
uint16 CC = 32768;
// partial normal results recorded for the previous line
TArray<FVector3f, TInlineAllocator<512>> PrevLinePartialNormals;
PrevLinePartialNormals.SetNumZeroed(Width);
// iterate each line
for (int32 Y = 0; Y < Height; Y++)
{
int32 LineOffsetInPixels = Y * Width;
uint8* Src = &SourceData[LineOffsetInPixels * 2];
FColor* Dst = (FColor*)&DestData[LineOffsetInPixels * 4];
if (Y == 0)
{
// just decode heights for the first line (normals don't matter they will be stomped below)
for (int32 X = 0; X < Width; X++)
{
uint16 DeltaHeight = Src[0] * 256 + Src[1];
CC += DeltaHeight;
*Dst = FColor(CC >> 8, CC & 0xff, 128, 128);
Src += 2;
Dst++;
}
}
else
{
// compute initial values (first pixel)
FVector3f P1(ForceInitToZero), P01(ForceInitToZero); // previous quad N1 and (N0+N1) normals
uint16 TT; // previous quad TT height
{
uint16 DeltaHeight = Src[0] * 256 + Src[1];
CC += DeltaHeight;
*Dst = FColor(CC >> 8, CC & 0xff, 128, 128);
// load TT for first pixel (becomes TL for second pixel)
TT = DecodeHeightU16(Dst + 0 - Width);
Src += 2;
Dst++;
}
// rest of the pixels in the line
for (int32 X = 1; X < Width; X++)
{
// re-use previous pixel TT and CC as this pixel TL and LL
uint16 TL = TT;
uint16 LL = CC;
// 1) Decode Height at CC
uint16 DeltaHeight = Src[0] * 256 + Src[1];
CC += DeltaHeight;
// load TT
TT = DecodeHeightU16(Dst + 0 - Width);
// 2) Write Height at CC (normals get written during processing of the next line)
*Dst = FColor(CC >> 8, CC & 0xff, 128, 128);
// 3) Compute local normals N0/N1 for the current quad (CC/TT/TL/LL)
FVector3f N0 = ComputeGridNormalFromDeltaHeightsPremultU16(CC - LL, LL - TL, PremultU16);
FVector3f N1 = ComputeGridNormalFromDeltaHeightsPremultU16(TT - TL, CC - TT, PremultU16);
FVector3f N01 = N0 + N1;
// 4) Complete Normal calculation for TL - this takes the partial result from the previous line and fills in the rest
FVector3f TL_Normal = PrevLinePartialNormals[X - 1] + P1 + N01;
FastNormalize(TL_Normal);
// 5) Write Normal for TL
Dst[-Width - 1].B = static_cast<uint8>(FMath::Clamp((TL_Normal.X * 127.5f + 127.5f), 0.0f, 255.0f));
Dst[-Width - 1].A = static_cast<uint8>(FMath::Clamp((TL_Normal.Y * 127.5f + 127.5f), 0.0f, 255.0f));
// 6) Store Partial Normal for LL in PrevLinePartialNormals (P0 + P1 + N0) - the rest will be filled in when processing the next line
FVector3f LL_PartialNormal = P01 + N0;
PrevLinePartialNormals[X - 1] = LL_PartialNormal;
// pass normals to next pixel
P1 = N1;
P01 = N01;
Src += 2;
Dst++;
}
}
}
// write out normals along the edge (delta encoded clockwise starting from top left)
{
TRACE_CPUPROFILER_EVENT_SCOPE(EdgeNormalFixup);
uint8* Src = &SourceData[TotalPixels * 2];
uint8 LastNormalX = 128;
uint8 LastNormalY = 128;
auto DecodeNormal = [&LastNormalX, &LastNormalY, DestData, Width, &Src](int32 X, int32 Y)
{
int32 DestOffset = (Y * Width + X) * 4;
LastNormalX += Src[0];
LastNormalY += Src[1];
DestData[DestOffset + 0] = LastNormalX;
DestData[DestOffset + 3] = LastNormalY;
Src += 2;
};
for (int32 X = 0; X < Width; X++) // [0 ... Width-1], 0
{
DecodeNormal(X, 0);
}
for (int32 Y = 1; Y < Height; Y++) // Width-1, [1 ... Height-1]
{
DecodeNormal(Width - 1, Y);
}
for (int32 X = Width - 2; X >= 0; X--) // [Width-2 ... 0], Height-1
{
DecodeNormal(X, Height - 1);
}
for (int32 Y = Height - 2; Y >= 1; Y--) // 0, [Height-2 ... 1]
{
DecodeNormal(0, Y);
}
check(Src == &SourceData[SourceDataBytes]);
}
}
#undef PROVIDER_DEBUG_LOG
#undef PROVIDER_DEBUG_LOG_DETAIL