// 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(); if (Factory == nullptr) { // create a new one (with TargetTexture as outer) Factory = NewObject(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(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 static bool SerializeArray(FArchive& Ar, TArray& 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(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(Owner->GetCachedLODBias() - Owner->NumCinematicMipLevels, 0, TotalMips - 1); const int32 MaxRuntimeMipCount = FMath::Min(GMaxTextureMipCount, FStreamableRenderResourceState::MAX_LOD_COUNT); const int32 NumMips = FMath::Min(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(NumMips, NumNonOptionalMips); const int32 NumOfNonStreamingMips = FMath::Min(NumMips, NumNonStreamingMips); // Optional mips must be streaming mips : check(BiasedNumNonOptionalMips >= NumOfNonStreamingMips); if (NumOfNonStreamingMips == NumMips) { bTextureIsStreamable = false; } const int32 AssetMipIdxForResourceFirstMip = FMath::Max(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(0, Owner->GetCachedLODBias() - AssetMipIdxForResourceFirstMip); // Ensure NumMipsInTail is within valid range to safeguard on the above expressions. const int32 NumMipsInTail = FMath::Clamp(PlatformNumMipsInTail, 1, NumMips); // Bias is not allowed to shrink the mip count below NumMipsInTail. NumRequestedMips = FMath::Max(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(NumOfNonStreamingMips); PostInitState.NumNonOptionalLODs = IntCastChecked(BiasedNumNonOptionalMips); PostInitState.MaxNumLODs = IntCastChecked(NumMips); PostInitState.AssetLODBias = IntCastChecked(AssetMipIdxForResourceFirstMip); PostInitState.NumResidentLODs = IntCastChecked(NumRequestedMips); PostInitState.NumRequestedLODs = IntCastChecked(NumRequestedMips); return PostInitState; } bool ULandscapeTextureStorageProviderFactory::GetInitialMipData(int32 FirstMipToLoad, TArrayView OutMipData, TArrayView 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(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(); if (Factory == nullptr) { // create a new one Factory = NewObject(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 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(FMemory::Malloc(StreamDataSize)); TRACE_IOSTORE_METADATA_SCOPE_TAG("Landscape"); IORequests[StartingMipIndex].BulkDataIORequest.Reset( SourceMip->BulkData.CreateStreamingRequest( 0, StreamDataSize, (EAsyncIOPriorityAndFlags)FMath::Clamp(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(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(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> 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(FMath::Clamp((TL_Normal.X * 127.5f + 127.5f), 0.0f, 255.0f)); Dst[-Width - 1].A = static_cast(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