You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Currently, Landscape will only apply the new storage provider to it's heightmaps at cook time, when it sees that the landscape has been flagged to use the new compressed storage path (enabled under Landscape -> Advanced), and only for Windows platforms (to be expanded as we test other platforms).
The compressed storage path option on the landscape is hidden by default, and can be made visible via CVAR ("landscape.ShowCompressHeightMapsOption")
Makes use of a new All Mip Provider interface to Texture2Ds, that expands on the existing Mip Data Provider (that only supported providing streaming mips).
The new interface assumes you want to completely override the Texture2D mip data.
When Texture2D sees that it has an All Mip Provider, it will skip serializing it's own heavyweight mip data during cook, as it assumes the provider will take care of providing all mip data.
The majority of the implementation is in LandscapeTextureStorageProvider, which implements the AllMipProvider that can store landscape heightmaps in a custom compressed format. Each mip's height data is stored using delta encoding, and normals are reconstructed.
#rb jonathan.bard, jian.ru
#jira UE-180654
#preflight 64541acf6c35ad81e61890cc
#preflight 64542713fd4b8f4e0d9a2ce9
#preflight 6459378bfd4b8f4e0dc206d2
[CL 25381191 by chris tchou in ue5-main branch]
794 lines
28 KiB
C++
794 lines
28 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 "LandscapePrivate.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(LandscapeTextureStorageProvider)
|
|
|
|
FLandscapeTextureStorageMipProvider::FLandscapeTextureStorageMipProvider(ULandscapeTextureStorageProviderFactory* InFactory)
|
|
: FTextureMipDataProvider(InFactory->Texture, ETickState::Init, ETickThread::Async)
|
|
{
|
|
this->Factory = InFactory;
|
|
|
|
if (InFactory->Texture)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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;
|
|
bool bDiscardInternalCopy = true;
|
|
Mip.BulkData.GetCopy(&SourceData, bDiscardInternalCopy);
|
|
check(SourceData);
|
|
uint8* DestData = static_cast<uint8*>(FMemory::Malloc(DestBytes));
|
|
DecompressMip((uint8*) SourceData, BulkDataSize, DestData, DestBytes, MipIndex);
|
|
OutMipData[MipIndex - FirstMipToLoad] = DestData;
|
|
FMemory::Free(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* TargetTexture, const FVector& InLandsapeGridScale)
|
|
{
|
|
check(TargetTexture);
|
|
check(TargetTexture->Source.IsValid())
|
|
|
|
check(TargetTexture->Source.GetFormat() == TSF_BGRA8);
|
|
EPixelFormat Format = PF_B8G8R8A8;
|
|
|
|
int32 Width = TargetTexture->Source.GetSizeX();
|
|
int32 Height = TargetTexture->Source.GetSizeY();
|
|
int32 MipCount = TargetTexture->Source.GetNumMips();
|
|
|
|
uint32 SrcBpp = GPixelFormats[Format].BlockBytes;
|
|
uint32 SrcPitch = Width * SrcBpp;
|
|
|
|
// try to get an existing factory
|
|
ULandscapeTextureStorageProviderFactory* Factory= TargetTexture->GetAssetUserData<ULandscapeTextureStorageProviderFactory>();
|
|
if (Factory == nullptr)
|
|
{
|
|
// create a new one
|
|
Factory = NewObject<ULandscapeTextureStorageProviderFactory>(TargetTexture);
|
|
Factory->Texture = TargetTexture;
|
|
TargetTexture->AddAssetUserData(Factory);
|
|
}
|
|
|
|
Factory->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 NumNonStreamingMips = 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;
|
|
|
|
NumNonStreamingMips = FMath::Max(NumNonStreamingMips, NumMipsInTail);
|
|
NumNonStreamingMips = FMath::Max(NumNonStreamingMips, UTexture2D::GetStaticMinTextureResidentMipCount());
|
|
NumNonStreamingMips = FMath::Min(NumNonStreamingMips, MipCount);
|
|
Factory->NumNonStreamingMips = NumNonStreamingMips;
|
|
}
|
|
|
|
// calculate number of non-optional mips
|
|
{
|
|
// for now, landscape texture storage does not have any optional mips
|
|
Factory->NumNonOptionalMips = MipCount;
|
|
}
|
|
|
|
Factory->Mips.Empty();
|
|
int32 MipWidth = Width;
|
|
int32 MipHeight = Height;
|
|
for (int32 MipIndex = 0; MipIndex < MipCount; MipIndex++)
|
|
{
|
|
FLandscapeTexture2DMipMap* Mip = new(Factory->Mips) FLandscapeTexture2DMipMap();
|
|
Mip->SizeX = MipWidth;
|
|
Mip->SizeY = MipHeight;
|
|
|
|
TArray64<uint8> MipData;
|
|
TargetTexture->Source.GetMipData(MipData, MipIndex);
|
|
|
|
// store small mips uncompressed
|
|
constexpr int32 UncompressedMipSizeThreshold = 8;
|
|
if ((MipWidth <= UncompressedMipSizeThreshold) || (MipHeight <= UncompressedMipSizeThreshold))
|
|
{
|
|
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 >>= 1;
|
|
MipHeight >>= 1;
|
|
}
|
|
|
|
return Factory;
|
|
}
|
|
#endif // WITH_EDITORONLY_DATA
|
|
|
|
|
|
// 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);
|
|
|
|
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;
|
|
}
|
|
|
|
// If Data size is specified, check compatibility for safety // TODO: size doesn't have to match when compressed...
|
|
if (DestMip.DataSize && (uint64)SourceMip->BulkData.GetBulkDataSize() > DestMip.DataSize)
|
|
{
|
|
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();
|
|
uint8* StreamData = static_cast<uint8*>(FMemory::Malloc(StreamDataSize));
|
|
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)
|
|
);
|
|
|
|
// 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)
|
|
{
|
|
// 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);
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
check(SourceDataBytes == TotalPixels * 4);
|
|
|
|
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).
|
|
static FVector ComputeTriangleNormal(const FVector& InPoint0, const FVector& InPoint1, const FVector& InPoint2)
|
|
{
|
|
FVector Normal = (InPoint0 - InPoint1).Cross(InPoint1 - InPoint2);
|
|
Normal.Normalize();
|
|
return Normal;
|
|
}
|
|
|
|
static 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);
|
|
}
|
|
|
|
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];
|
|
|
|
if (!Mip.bCompressed)
|
|
{
|
|
// mip is uncompressed, just copy it
|
|
check(SourceDataBytes == DestDataBytes);
|
|
memcpy(DestData, SourceData, DestDataBytes);
|
|
return;
|
|
}
|
|
|
|
check(Mip.bCompressed);
|
|
|
|
int32 TotalPixels = Mip.SizeX * Mip.SizeY;
|
|
check(SourceDataBytes == (TotalPixels + (Mip.SizeX + Mip.SizeY) * 2 - 4) * 2);
|
|
check(DestDataBytes == TotalPixels * 4);
|
|
|
|
// Undo Delta Encode of Heights
|
|
uint16 LastHeight = 32768;
|
|
for (int32 PixelIndex = 0; PixelIndex < TotalPixels; PixelIndex++)
|
|
{
|
|
int32 SourceOffset = PixelIndex * 2;
|
|
uint16 DeltaHeight = SourceData[SourceOffset + 0] * 256 + SourceData[SourceOffset + 1];
|
|
|
|
// undo delta
|
|
LastHeight += DeltaHeight;
|
|
|
|
// texture data is stored as BGRA, or [normal x, height low bits, height high bits, normal y]
|
|
int32 DestOffset = PixelIndex * 4;
|
|
DestData[DestOffset + 0] = 128;
|
|
DestData[DestOffset + 1] = LastHeight & 0xff;
|
|
DestData[DestOffset + 2] = LastHeight >> 8;
|
|
DestData[DestOffset + 3] = 128;
|
|
}
|
|
|
|
// recompute normals in the interior
|
|
{
|
|
// we skip computing the edges, as they will be overwritten later (and this way we don't have to handle samples that go off the edge)
|
|
for (int32 Y = 1; Y < Mip.SizeY - 1; Y++)
|
|
{
|
|
for (int32 X = 1; X < Mip.SizeX - 1; X++)
|
|
{
|
|
// based on shader code in LandscapeLayersPS.usf
|
|
FVector TL, TT, CC, LL, RR, BR, BB;
|
|
|
|
SampleWorldPositionAtOffset(TL, DestData, X - 1, Y - 1, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(TT, DestData, X + 0, Y - 1, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(CC, DestData, X + 0, Y + 0, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(LL, DestData, X - 1, Y + 0, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(RR, DestData, X + 1, Y + 0, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(BR, DestData, X + 1, Y + 1, Mip.SizeX, LandscapeGridScale);
|
|
SampleWorldPositionAtOffset(BB, DestData, X + 0, Y + 1, Mip.SizeX, LandscapeGridScale);
|
|
|
|
FVector N0 = ComputeTriangleNormal(CC, LL, TL);
|
|
FVector N1 = ComputeTriangleNormal(TL, TT, CC);
|
|
FVector N2 = ComputeTriangleNormal(LL, CC, BB);
|
|
FVector N3 = ComputeTriangleNormal(RR, CC, TT);
|
|
FVector N4 = ComputeTriangleNormal(BR, BB, CC);
|
|
FVector N5 = ComputeTriangleNormal(CC, RR, BR);
|
|
|
|
FVector FinalNormal = (N0 + N1 + N2 + N3 + N4 + N5);
|
|
FinalNormal.Normalize();
|
|
|
|
// rescale normal.xy to [0,255] range, and write out as bytes
|
|
int32 OffsetBytes = (Y * Mip.SizeX + X) * 4;
|
|
DestData[OffsetBytes + 0] = static_cast<uint8>(FMath::Clamp(((FinalNormal.X + 1.0) * 0.5) * 255.0, 0.0, 255.0));
|
|
DestData[OffsetBytes + 3] = static_cast<uint8>(FMath::Clamp(((FinalNormal.Y + 1.0) * 0.5) * 255.0, 0.0, 255.0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// write out normals along the edge (delta encoded clockwise starting from top left)
|
|
int32 SourceOffset = TotalPixels * 2;
|
|
uint8 LastNormalX = 128;
|
|
uint8 LastNormalY = 128;
|
|
|
|
auto DecodeNormal = [&LastNormalX, &LastNormalY, SourceData, DestData, MipSizeX = Mip.SizeX, &SourceOffset](int32 X, int32 Y)
|
|
{
|
|
int32 DestOffset = (Y * MipSizeX + X) * 4;
|
|
LastNormalX += SourceData[SourceOffset + 0];
|
|
LastNormalY += SourceData[SourceOffset + 1];
|
|
DestData[DestOffset + 0] = LastNormalX;
|
|
DestData[DestOffset + 3] = LastNormalY;
|
|
SourceOffset += 2;
|
|
};
|
|
|
|
for (int32 X = 0; X < Mip.SizeX; X++) // [0 ... MipSizeX-1], 0
|
|
{
|
|
DecodeNormal(X, 0);
|
|
}
|
|
|
|
for (int32 Y = 1; Y < Mip.SizeY; Y++) // MipSizeX-1, [1 ... MipSizeY-1]
|
|
{
|
|
DecodeNormal(Mip.SizeX - 1, Y);
|
|
}
|
|
|
|
for (int32 X = Mip.SizeX - 2; X >= 0; X--) // [MipSizeX-2 ... 0], MipSizeY-1
|
|
{
|
|
DecodeNormal(X, Mip.SizeY - 1);
|
|
}
|
|
|
|
for (int32 Y = Mip.SizeY - 2; Y >= 1; Y--) // 0, [MipSizeY-2 ... 1]
|
|
{
|
|
DecodeNormal(0, Y);
|
|
}
|
|
|
|
check(SourceOffset == SourceDataBytes);
|
|
}
|