Files
UnrealEngineUWP/Engine/Source/Runtime/Landscape/Private/LandscapeUtils.cpp
jonathan bard c70b8af6e7 Introducing landscape batched merge (activated by CVar : landscape.EditLayersLocalMerge.Enable 2): complete refactor of the landscape edit layers merge algorithm to solve the taxing GPU memory requirements that global merge has and serve as the basis for future features (new weight-blending methods, partial exports, ...) and performance and workflow improvements (deterministic editing of partially-loaded worlds, local updates, removal of useless weightmap allocations, ...)
Unlike local merge (landscape.EditLayersLocalMerge.Enable 1), batched merge is backwards-compatible with BP brushes and will therefore replace both global merge and local merge in the near future. However, in order to maintain full backwards-compatibility, existing BP brushes will still act as they do now wrt determinism, requiring the entire world to be loaded by default. This means that as long as a landscape uses existing and not-yet converted brushes like Landmass, Water, etc., it will effectively act as global merge, with the same memory constraints as the current ones.
It also supports both vertical (in between edit layers) and horizontal (in between paint layers) blending, which allows legacy weight-blending to still work but opens up several possibilities in term of paint layers blending improvements.

Details:
* ILandscapeEditLayerRenderer is an interface (UInterface, actually) to implement for any element that needs to write heightmaps/weightmaps/visibility maps. These can be anything (edit layers, BP brushes, actors, components, ...), which streamlines the rendering/blending capabilities while maintaining backwards-compatibility
* UE::Landscape::EditLayers::IEditLayerRendererProvider is an interface to implement in order to provide a sequence of ILandscapeEditLayerRenderer to the landscape. This will be called on-demand when merging the landscape, which means the landscape dynamically retrieves its active renderers instead of relying on the landscape actor's data to do so. This solves the issue of having to checkout the landscape when adding/removing an element (e.g. a BP brush) to it. BP brushes are still supported, but things like landscape patch components can now be trivially added to a sequence of render operations without checking out the landscape actor
* The merge operation is now generic and is split into several render batches (instead of one large render, the size of the entire loaded landscape), hence the name. Each batch is sized automatically so that it contains a minimal amount of weightmaps and a minimal amount of landscape components.
* A component dependency analysis is run in order to maximize the amount of components to render in a given batch while trying to respect the optimal batch size (CVar landscape.EditLayersLocalMerge.MaxResolutionPerRenderBatch, default value : 1024) If a given component depends on components outside the batch, though, it will either be rendered in another batch (if possible) or the batches will be made large enough to render these (which allows the system to be compatible with existing BP brushes such as Water, but also prevents batched merge to solve the GPU memory requirements that global merge has)
* The merge is split into the following phases :
1. Retrieval of edit layer renderers and their associated states for the various merge types (supported/enabled for heightmaps/weightmaps/visibility)
At this point, the user can selectively change the enabled state of the supported renderers, which allows to customize the merge operation without mutating the landscape state (e.g. only keeping the layers under edit layer N enabled)
2. Trim the unneeded renderers for the requested merge operation (disabled renderers, unneeded weightmaps, etc.)
3. (weightmap merge only) Analyze the renderers' weightmaps usage and divide the renders into several "render groups", that is, a set of weightmaps rendered together because they have a(n horizontal) dependency. Each render group will be rendered separately (i.e. reduces the memory requirements of weight-blending to the minimum)
4. Each renderer provides a list of "render items", which contain locality information (where does this render item outputs data? what input data does it require? does it output heightmap/weightmap/visibility? what weightmap(s) does it output?) and a full component dependency analysis is then run on all renderers/requested components
5. The work is split into several batches, each batch into several render steps : for each render group, there's one step per active renderer, and one last step : a simple callback, which the user can attach to, which allows to retrieve the data that has been rendered for this render group for this batch, i.e. a portion of the merge landscape data, before the render target it was using gets repurposed for another batch/render group.
Currently, this is where the readback of heightmaps/weightmaps to the final texture is done and eventually, the same merge can be repurposed by using another callback and, for example, copying into a transient render target (partial renders), or an image on disk (partial exports)
6. The list of render batches and render steps is executed. Some renderers are automatically added to mix (e.g. default data rendering, to provide the initial clear, at the beginning, and "legacy" weight-blending and normals rendering, both at the end of the merge process) and each batch also has a list of "validity" render targets (one per target layer), which is nothing else than a binary "stencil" buffer, masking the components that are *not* in a given batch. These are not implemented as stencil buffers, though, because we eventually might need access to them on the BP side, so they are plain UTextureRenderTarget2D.
* Several CVars (landscape.BatchedMerge.VisualLog.*) were added to help debug the merge process
* In order to improve rendering time, a new utility shader (CopyQuadsMultiSourcePS) was made  to copy several (up to 63) source textures into an atlas, which optimizes quite a bit the edit layer->atlas copy operation

Misc changes:
* Improvements/fixes to landscape-related GPU tags
* Fixed landscape.DumpWeightmapDiff which would leave txt/png files for weightmap allocations that haven't actually changed if one of the other channels of the weightmap had some data changes

Code details:
* OnFullHeightmapRenderDoneDelegate has been replaced with OnEditLayersMerged and is now called for each batch render
* Non-public API landscape utils have moved to LandscapeUtilsPrivate.h/.cpp
* Fixed edge case of a map that was saved with an invalid ULandscapeEditLayer (e.g. after having uninstalled a plugin where the base class of a layer was defined): we now clear the layer and make sure we don't fall into a situation where there's no edit layer altogether (which is not allowed and usually enforced by the UX)
* Added parameter to ReallocateWeightmaps in order to restrict the re-allocation of weightmaps to a given set of components. This is to avoid a situation where we're trying to add weightmap allocations to components that are not part of the merge. This will eventually go away when we refactor the weightmap allocation system in the editor
* Since the whole thing has to run on the game thread (because of BP calls), a new "scratch render target" system has been added (ULandscapeEditResourcesSubsystem / ULandscapeScratchRenderTarget) in order to help the various merge steps to request/release URenderTargets (2D and 2D array) and manage their RHI access. This would normally be done via RDG but we don't have access to it outside the render thread. This is shared across landscapes, which minimizes the amount of render targets needed when editing multiple landscapes at the same time.

#rb chris.tchou
#jira UE-214800
#tests Editor, cooked game

[CL 35108473 by jonathan bard in ue5-main branch]
2024-07-26 10:30:17 -04:00

289 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LandscapeUtils.h"
#include "Engine/Level.h"
#include "DataDrivenShaderPlatformInfo.h"
#include "LandscapeLayerInfoObject.h"
#include "LandscapeProxy.h"
#include "LandscapeEditTypes.h"
#include "Algo/Transform.h"
#if WITH_EDITOR
#include "EditorDirectories.h"
#include "Engine/Texture2D.h"
#include "LandscapeComponent.h"
#include "ObjectTools.h"
#include "AssetRegistry/AssetRegistryModule.h"
#endif
// Channel remapping
extern const size_t ChannelOffsets[4];
namespace UE::Landscape
{
bool DoesPlatformSupportEditLayers(EShaderPlatform InShaderPlatform)
{
// Edit layers work on the GPU and are only available on SM5+ and in the editor :
return IsFeatureLevelSupported(InShaderPlatform, ERHIFeatureLevel::SM5)
&& !IsConsolePlatform(InShaderPlatform)
&& !IsMobilePlatform(InShaderPlatform);
}
ELandscapeToolTargetTypeFlags GetLandscapeToolTargetTypeAsFlags(ELandscapeToolTargetType InTargetType)
{
uint8 TargetTypeValue = static_cast<uint8>(InTargetType);
check(TargetTypeValue < static_cast<uint8>(ELandscapeToolTargetType::Count));
return static_cast<ELandscapeToolTargetTypeFlags>(1 << TargetTypeValue);
}
ELandscapeToolTargetType GetLandscapeToolTargetTypeSingleFlagAsType(ELandscapeToolTargetTypeFlags InSingleFlag)
{
check(FMath::CountBits(static_cast<uint64>(InSingleFlag)) == 1);
uint32 Index = FMath::FloorLog2(static_cast<uint8>(InSingleFlag));
check(Index < static_cast<uint32>(ELandscapeToolTargetType::Count));
return static_cast<ELandscapeToolTargetType>(Index);
}
FString GetLandscapeToolTargetTypeFlagsAsString(ELandscapeToolTargetTypeFlags InTargetTypeFlags)
{
TArray<FString> TargetTypeStrings;
Algo::Transform(MakeFlagsRange(InTargetTypeFlags), TargetTypeStrings, [](ELandscapeToolTargetTypeFlags InTargetTypeFlag)
{ return UEnum::GetValueAsString(GetLandscapeToolTargetTypeSingleFlagAsType(InTargetTypeFlag)); });
return *FString::Join(TargetTypeStrings, TEXT(","));
}
#if WITH_EDITOR
FString GetSharedAssetsPath(const FString& InPath)
{
FString Path = InPath + TEXT("_sharedassets/");
if (Path.StartsWith("/Temp/"))
{
Path = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::LEVEL) / Path.RightChop(FString("/Temp/").Len());
}
return Path;
}
FString GetSharedAssetsPath(const ULevel* InLevel)
{
return GetSharedAssetsPath(InLevel->GetOutermost()->GetName());
}
FString GetLayerInfoObjectPackageName(const ULevel* InLevel, const FName& InLayerName, FName& OutLayerObjectName)
{
FString PackageName;
FString PackageFilename;
FString SharedAssetsPath = GetSharedAssetsPath(InLevel);
int32 Suffix = 1;
OutLayerObjectName = FName(*FString::Printf(TEXT("%s_LayerInfo"), *ObjectTools::SanitizeInvalidChars(*InLayerName.ToString(), INVALID_LONGPACKAGE_CHARACTERS)));
FPackageName::TryConvertFilenameToLongPackageName(SharedAssetsPath / OutLayerObjectName.ToString(), PackageName);
while (FPackageName::DoesPackageExist(PackageName, &PackageFilename))
{
OutLayerObjectName = FName(*FString::Printf(TEXT("%s_LayerInfo_%d"), *ObjectTools::SanitizeInvalidChars(*InLayerName.ToString(), INVALID_LONGPACKAGE_CHARACTERS), Suffix));
if (!FPackageName::TryConvertFilenameToLongPackageName(SharedAssetsPath / OutLayerObjectName.ToString(), PackageName))
{
break;
}
Suffix++;
}
return PackageName;
}
bool IsVisibilityLayer(const ULandscapeLayerInfoObject* InLayerInfoObject)
{
return (ALandscapeProxy::VisibilityLayer != nullptr) && (ALandscapeProxy::VisibilityLayer == InLayerInfoObject);
}
uint32 GetTypeHash(const FTextureCopyRequest& InKey)
{
uint32 Hash = ::GetTypeHash(InKey.Source);
uint32 HashSlice = ::GetTypeHash(InKey.DestinationSlice);
return HashCombine(Hash, ::GetTypeHash(InKey.Destination));
}
bool operator==(const FTextureCopyRequest& InEntryA, const FTextureCopyRequest& InEntryB)
{
return (InEntryA.Source == InEntryB.Source) && (InEntryA.Destination == InEntryB.Destination) && (InEntryA.DestinationSlice == InEntryB.DestinationSlice);
}
bool FBatchTextureCopy::AddWeightmapCopy(UTexture* InDestination, int8 InDestinationSlice, int8 InDestinationChannel, const ULandscapeComponent* InComponent, ULandscapeLayerInfoObject* InLayerInfo)
{
FTextureCopyRequest CopyRequest;
const TArray<UTexture2D*>& ComponentWeightmapTextures = InComponent->GetWeightmapTextures();
const TArray<FWeightmapLayerAllocationInfo>& ComponentWeightmapLayerAllocations = InComponent->GetWeightmapLayerAllocations();
int8 SourceChannel = INDEX_NONE;
CopyRequest.Destination = InDestination;
CopyRequest.DestinationSlice = InDestinationSlice;
// Find the proper Source Texture and channel from Layer Allocations
for (const FWeightmapLayerAllocationInfo& ComponentWeightmapLayerAllocation : ComponentWeightmapLayerAllocations)
{
if ((ComponentWeightmapLayerAllocation.LayerInfo == InLayerInfo) &&
ComponentWeightmapLayerAllocation.IsAllocated() &&
ComponentWeightmapTextures.IsValidIndex(ComponentWeightmapLayerAllocation.WeightmapTextureIndex))
{
CopyRequest.Source = ComponentWeightmapTextures[ComponentWeightmapLayerAllocation.WeightmapTextureIndex];
SourceChannel = ComponentWeightmapLayerAllocation.WeightmapTextureChannel;
break;
}
}
// Check if we found a proper allocation for this LayerInfo
if (SourceChannel != INDEX_NONE)
{
check((InDestinationChannel < 4) && (SourceChannel < 4));
FTextureCopyChannelMapping& ChannelMapping = CopyRequests.FindOrAdd(MoveTemp(CopyRequest));
ChannelMapping[ChannelOffsets[InDestinationChannel]] = ChannelOffsets[SourceChannel];
return true;
}
return false;
}
struct FSourceDataMipNumber
{
TOptional<FTextureSource::FMipData> MipData;
int32 MipNumber = 0;
};
struct FDestinationDataMipNumber
{
TArray<uint8*> DestinationDataPtr;
int32 MipNumber = 0;
};
bool FBatchTextureCopy::ProcessTextureCopies()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FBatchTextureCopy::ProcessTextureCopyRequest);
TMap<UTexture2D*, FSourceDataMipNumber> Sources;
TMap<UTexture*, FDestinationDataMipNumber> Destinations;
if (CopyRequests.Num() == 0)
{
return false;
}
// Populate source/destination maps to filter unique occurrences
for (const TPair<FTextureCopyRequest, FTextureCopyChannelMapping>& CopyRequest : CopyRequests)
{
FSourceDataMipNumber& SourceData = Sources.Add(CopyRequest.Key.Source);
SourceData.MipNumber = CopyRequest.Key.Source->Source.GetNumMips();
FDestinationDataMipNumber& DestinationData = Destinations.Add(CopyRequest.Key.Destination);
DestinationData.MipNumber = CopyRequest.Key.Destination->Source.GetNumMips();
}
// Decompress (if needed) and get the source textures ready for access
for (TPair<UTexture2D*, FSourceDataMipNumber>& Source : Sources)
{
Source.Value.MipData = Source.Key->Source.GetMipData(nullptr);
}
// Lock all destinations mips
for (TPair<UTexture*, FDestinationDataMipNumber>& Destination : Destinations)
{
int32 MipNumber = Destination.Value.MipNumber;
TArray<uint8*>& DestinationDataPtr = Destination.Value.DestinationDataPtr;
for (int32 MipLevel = 0; MipLevel < MipNumber; ++MipLevel)
{
DestinationDataPtr.Add(Destination.Key->Source.LockMip(MipLevel));
}
}
for (const TPair<FTextureCopyRequest, FTextureCopyChannelMapping>& CopyRequest : CopyRequests)
{
const FSourceDataMipNumber* SourceDataMipNumber = Sources.Find(CopyRequest.Key.Source);
const FDestinationDataMipNumber* DestinationDataMipNumber = Destinations.Find(CopyRequest.Key.Destination);
check((SourceDataMipNumber != nullptr) && (DestinationDataMipNumber != nullptr));
check(SourceDataMipNumber->MipNumber == DestinationDataMipNumber->MipNumber);
const int32 MipNumber = SourceDataMipNumber->MipNumber;
for (int32 MipLevel = 0; MipLevel < MipNumber; ++MipLevel)
{
const int64 MipSizeInBytes = CopyRequest.Key.Source->Source.CalcMipSize(MipLevel);
const int32 MipSize = CopyRequest.Key.Destination->Source.GetSizeX() >> MipLevel;
check(MipSize == (CopyRequest.Key.Destination->Source.GetSizeY() >> MipLevel));
int32 MipSizeSquare = FMath::Square(MipSize);
FSharedBuffer MipSrcData = SourceDataMipNumber->MipData->GetMipData(0, 0, MipLevel);
const uint8* SourceTextureData = static_cast<const uint8*>(MipSrcData.GetData());
uint8* DestTextureData = DestinationDataMipNumber->DestinationDataPtr[MipLevel] + CopyRequest.Key.DestinationSlice * MipSizeInBytes;
check((SourceTextureData != nullptr) && (DestTextureData != nullptr));
const FTextureCopyChannelMapping& ChannelMapping = CopyRequest.Value;
// Perform the copy, redirecting channels using mappings
for (int32 Index = 0; Index < MipSizeSquare; ++Index)
{
int32 Base = Index * 4;
for (int32 Channel = 0; Channel < 4; ++Channel)
{
if (ChannelMapping[Channel] == INDEX_NONE)
{
continue;
}
DestTextureData[Base + Channel] = SourceTextureData[Base + ChannelMapping[Channel]];
}
}
}
}
// Note that source textures do not need unlocking, data will be released once the FMipData go out of scope
// Unlock all destination mips
for (TPair<UTexture*, FDestinationDataMipNumber>& Destination : Destinations)
{
int32 MipNumber = Destination.Value.MipNumber;
for (int32 MipLevel = 0; MipLevel < MipNumber; ++MipLevel)
{
Destination.Key->Source.UnlockMip(MipLevel);
}
}
return true;
}
FLayerInfoFinder::FLayerInfoFinder()
{
const UClass* AssetClass = ULandscapeLayerInfoObject::StaticClass();
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FARFilter Filter;
FName PackageName = *AssetClass->GetPackage()->GetName();
FName AssetName = AssetClass->GetFName();
Filter.ClassPaths.Add(FTopLevelAssetPath(PackageName,AssetName));
AssetRegistryModule.Get().GetAssets(Filter, LayerInfoAssets);
}
ULandscapeLayerInfoObject* FLayerInfoFinder::Find(const FName& LayerName) const
{
for (const FAssetData& LayerInfoAsset : LayerInfoAssets)
{
ULandscapeLayerInfoObject* LayerInfo = CastChecked<ULandscapeLayerInfoObject>(LayerInfoAsset.GetAsset());
if (LayerInfo && LayerInfo->LayerName == LayerName)
{
return LayerInfo;
}
}
return nullptr;
}
#endif //!WITH_EDITOR
} // end namespace UE::Landscape