// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Async/TaskGraphInterfaces.h" #include "Containers/EnumAsByte.h" #include "Containers/HashTable.h" #include "CoreMinimal.h" #include "Logging/LogMacros.h" #include "Math/Color.h" #include "Math/IntVector.h" #include "Math/UnrealMathSSE.h" #include "Misc/AssertionMacros.h" #include "Misc/EnumClassFlags.h" #include "PixelFormat.h" #include "Stats/Stats.h" #include "Templates/RefCounting.h" #include "Templates/TypeHash.h" #include "UObject/NameTypes.h" class FRDGBuilder; class FRHICommandListBase; class FRHIShaderResourceView; class FRHITexture; class FRHIUnorderedAccessView; struct IPooledRenderTarget; class ISceneRenderer; namespace ERHIFeatureLevel { enum Type : int; } enum class EVTProducerPriority : uint8; enum class EVTInvalidatePriority : uint8; union FVirtualTextureProducerHandle { FVirtualTextureProducerHandle() : PackedValue(0u) {} explicit FVirtualTextureProducerHandle(uint32 InPackedValue) : PackedValue(InPackedValue) {} FVirtualTextureProducerHandle(uint32 InIndex, uint32 InMagic) : Index(InIndex), Magic(InMagic) {} inline bool IsValid() const { return PackedValue != 0u; } inline bool IsNull() const { return PackedValue == 0u; } uint32 PackedValue; struct { uint32 Index : 22; uint32 Magic : 10; }; friend inline bool operator==(const FVirtualTextureProducerHandle& Lhs, const FVirtualTextureProducerHandle& Rhs) { return Lhs.PackedValue == Rhs.PackedValue; } friend inline bool operator!=(const FVirtualTextureProducerHandle& Lhs, const FVirtualTextureProducerHandle& Rhs) { return Lhs.PackedValue != Rhs.PackedValue; } }; static_assert(sizeof(FVirtualTextureProducerHandle) == sizeof(uint32), "Bad packing"); /** Maximum number of layers that can be allocated in a single VT page table */ #define VIRTUALTEXTURE_SPACE_MAXLAYERS 8 /** Maximum dimension of VT page table texture */ #define VIRTUALTEXTURE_LOG2_MAX_PAGETABLE_SIZE 12u #define VIRTUALTEXTURE_MAX_PAGETABLE_SIZE (1u << VIRTUALTEXTURE_LOG2_MAX_PAGETABLE_SIZE) #define VIRTUALTEXTURE_MIN_PAGETABLE_SIZE 32u /** * Parameters needed to create an IAllocatedVirtualTexture * Describes both page table and physical texture size, format, and layout */ struct FAllocatedVTDescription { FName Name; uint32 TileSize = 0u; uint32 TileBorderSize = 0u; uint32 MaxSpaceSize = 0u; uint32 IndirectionTextureSize = 0u; uint8 Dimensions = 0u; uint8 NumTextureLayers = 0u; uint8 ForceSpaceID = 0xff; uint8 AdaptiveLevelBias = 0u; /** Producer for each texture layer. */ FVirtualTextureProducerHandle ProducerHandle[VIRTUALTEXTURE_SPACE_MAXLAYERS]; /** Local layer inside producer for each texture layer. */ uint8 ProducerLayerIndex[VIRTUALTEXTURE_SPACE_MAXLAYERS] = { 0u }; union { uint8 PackedFlags = 0u; struct { /** * Should the AllocatedVT create its own dedicated page table allocation? Can be useful to control total allocation. * The system only supports a limited number of unique page tables, so this must be used carefully */ uint8 bPrivateSpace : 1; /** * If the AllocatedVT has the same producer mapped to multiple layers, should those be merged into a single page table layer? * This can make for more efficient page tables when enabled, but certain code may make assumption that number of layers * specified when allocating VT exactly matches the resulting page page */ uint8 bShareDuplicateLayers : 1; }; }; friend inline bool operator==(const FAllocatedVTDescription& Lhs, const FAllocatedVTDescription& Rhs) { if (Lhs.TileSize != Rhs.TileSize || Lhs.TileBorderSize != Rhs.TileBorderSize || Lhs.Dimensions != Rhs.Dimensions || Lhs.NumTextureLayers != Rhs.NumTextureLayers || Lhs.PackedFlags != Rhs.PackedFlags) { return false; } for (uint32 LayerIndex = 0u; LayerIndex < Lhs.NumTextureLayers; ++LayerIndex) { if (Lhs.ProducerHandle[LayerIndex] != Rhs.ProducerHandle[LayerIndex] || Lhs.ProducerLayerIndex[LayerIndex] != Rhs.ProducerLayerIndex[LayerIndex]) { return false; } } return true; } friend inline bool operator!=(const FAllocatedVTDescription& Lhs, const FAllocatedVTDescription& Rhs) { return !operator==(Lhs, Rhs); } friend inline uint32 GetTypeHash(const FAllocatedVTDescription& Description) { uint32 Hash = GetTypeHash(Description.TileSize); Hash = HashCombine(Hash, GetTypeHash(Description.TileBorderSize)); Hash = HashCombine(Hash, GetTypeHash(Description.Dimensions)); Hash = HashCombine(Hash, GetTypeHash(Description.NumTextureLayers)); Hash = HashCombine(Hash, GetTypeHash(Description.PackedFlags)); for (uint32 LayerIndex = 0u; LayerIndex < Description.NumTextureLayers; ++LayerIndex) { Hash = HashCombine(Hash, GetTypeHash(Description.ProducerHandle[LayerIndex].PackedValue)); Hash = HashCombine(Hash, GetTypeHash(Description.ProducerLayerIndex[LayerIndex])); } return Hash; } }; struct FVTProducerDescription { RENDERCORE_API FVTProducerDescription(); FName Name; /** Will be name of UTexture for streaming VTs, mostly here for debugging */ uint32 FullNameHash; bool bPersistentHighestMip = true; bool bContinuousUpdate = false; bool bRequiresSinglePhysicalPool = false; bool bNotifyCompleted = false; /** Producer will receive OnRequestsCompleted() callbacks every frame when enabled. */ uint32 TileSize = 0u; uint32 TileBorderSize = 0u; /** * Producers are made up of a number of block, each block has uniform size, and blocks are arranged in a larger grid * 'Normal' VTs will typically be a single block, for UDIM textures, blocks will map to individual UDIM texture sheets * When multiple producers are allocated together, they will be aligned such that blocks of each layer overlay on top of each other * Number of blocks for each layer may be different in this case, this is handled by wrapping blocks for layers with fewer blocks */ uint32 BlockWidthInTiles = 0u; uint32 BlockHeightInTiles = 0u; uint32 DepthInTiles = 0u; uint16 WidthInBlocks = 1u; uint16 HeightInBlocks = 1u; uint8 Dimensions = 0u; uint8 MaxLevel = 0u; /** * Producers will fill a number of texture layers. * These texture layers can be distributed across one or more physical groups. * Each physical group can contain one or more of the texture layers. * Within a physical group the texture layers share the same UV allocation/mapping and can be referenced by a single page table lookup. */ uint8 NumTextureLayers = 0u; TEnumAsByte LayerFormat[VIRTUALTEXTURE_SPACE_MAXLAYERS] = { PF_Unknown }; FLinearColor LayerFallbackColor[VIRTUALTEXTURE_SPACE_MAXLAYERS] = { FLinearColor::Black }; bool bIsLayerSRGB[VIRTUALTEXTURE_SPACE_MAXLAYERS] = { false }; uint8 NumPhysicalGroups = 0u; uint8 PhysicalGroupIndex[VIRTUALTEXTURE_SPACE_MAXLAYERS] = { 0 }; /** Priority allows to process a producer before another which increases responsiveness in high contention situations */ EVTProducerPriority Priority; }; typedef void (FVTProducerDestroyedFunction)(const FVirtualTextureProducerHandle& InHandle, void* Baton); /** * A Finalizer is an object that does the final work of filling the physical textures. * The work for all finalizers is scheduled at a specific point in the frame where we can write to the physical texture without hazards, and * where the virtual texture page tables are also updated. * The finalizer work may be split into two parts. * RenderFinalize() does not write to the physical texture, but can read from virtual textures. This is a phase that allows any * page rendering which may need to sample virtual textures. Runtime virtual textures and material systems require this. * Finalise() must write to the physical textures, but cannot sample from them. * All finalizers need to implement Finalize() but only ones that need to sample virtual textures need to implement RenderFinalize(). */ class IVirtualTextureFinalizer { public: /** Finalize work that has read only access to the virtual texture physical pools. */ virtual void RenderFinalize(FRDGBuilder& GraphBuilder, ISceneRenderer* SceneRenderingContext) {} /** Finalize work that has write only access to the virtual texture physical pools. */ virtual void Finalize(FRDGBuilder& GraphBuilder) = 0; }; enum class EVTRequestPageStatus { /** The request is invalid and no data will ever be available */ Invalid, /** * Requested data is not being produced, and a request can't be started as some part of the system is at capacity. * Requesting the same data at a later time should succeed. */ Saturated, /** * Requested data is currently being produced, but is not yet ready. * It's valid to produce this data, but doing so may block until data is ready. */ Pending, /** Requested data is available */ Available, }; /** Check to see there is data available (possibly requiring waiting) given the current status */ FORCEINLINE bool VTRequestPageStatus_HasData(EVTRequestPageStatus InStatus) { return InStatus == EVTRequestPageStatus::Pending || InStatus == EVTRequestPageStatus::Available; } enum class EVTRequestPagePriority { Normal, High, }; enum class EVTProducePageFlags : uint8 { None = 0u, SkipPageBorders = (1u << 0), ContinuousUpdate = (1u << 1), }; ENUM_CLASS_FLAGS(EVTProducePageFlags); struct FVTRequestPageResult { FVTRequestPageResult(EVTRequestPageStatus InStatus = EVTRequestPageStatus::Invalid, uint64 InHandle = 0u) : Handle(InHandle), Status(InStatus) {} /** Opaque handle to the request, must be passed to 'ProducePageData'. Only valid if status is Pending/Available */ uint64 Handle; /** Status of the request */ EVTRequestPageStatus Status; }; /** Describes a location to write a single layer of a VT tile */ struct FVTProduceTargetLayer { /** Pooled render target to write to. */ IPooledRenderTarget* PooledRenderTarget = nullptr; /** Location within the target texture to write. */ FIntVector pPageLocation = FIntVector::ZeroValue; UE_DEPRECATED(5.6, "Use PooledRenderTarget instead.") FRHITexture* TextureRHI = nullptr; // Disable some TextureRHI deprecation warnings here. PRAGMA_DISABLE_DEPRECATION_WARNINGS FVTProduceTargetLayer() = default; FVTProduceTargetLayer(const FVTProduceTargetLayer&) = default; FVTProduceTargetLayer& operator=(const FVTProduceTargetLayer&) = default; PRAGMA_ENABLE_DEPRECATION_WARNINGS }; /** * This is the interface that can produce tiles of virtual texture data * This can be extended to represent different ways of generating VT, such as disk streaming, runtime compositing, or whatever * It's provided to the renderer module */ class IVirtualTexture { public: inline IVirtualTexture() {} virtual ~IVirtualTexture() {} /** * Gives a localized mip bias for the given local vAddress. * This is used to implement sparse VTs, the bias is number of mip levels to add to reach a resident page * Must be thread-safe, may be called from any thread * @param vLevel The mipmap level to check * @param vAddress Virtual address to check * @return Mip bias to be added to vLevel to reach a resident page at the given address */ virtual uint32 GetLocalMipBias(uint8 vLevel, uint32 vAddress) const { return 0u; } /** * Whether data for the given page is streamed (e.g. loading from disk). */ virtual bool IsPageStreamed(uint8 vLevel, uint32 vAddress) const = 0; /** * Makes a request for the given page data. * For data sources that can generate data immediately, it's reasonable for this method to do nothing, and simply return 'Available' * Only called from render thread * @param ProducerHandle Handle to this producer, can be used as a UID for this producer for any internal caching mechanisms * @param LayerMask Mask of requested layers * @param vLevel The mipmap level of the data * @param vAddress Bit-interleaved x,y page indexes * @param Priority Priority of the request, used to drive async IO/task priority needed to generate data for request * @return FVTRequestPageResult describing the availability of the request */ virtual FVTRequestPageResult RequestPageData(FRHICommandListBase& RHICmdList, const FVirtualTextureProducerHandle& ProducerHandle, uint8 LayerMask, uint8 vLevel, uint64 vAddress, EVTRequestPagePriority Priority) = 0; /** * Upload page data to the cache, data must have been previously requested, and reported either 'Available' or 'Pending' * The system will attempt to call RequestPageData/ProducePageData only once for a given vLevel/vAddress, with all the requested layers set in LayerMask, * this is important for certain types of procedural producers that may generate multiple layers of VT data at the same time * It's valid to produce 'Pending' page data, but in this case ProducePageData may block until data is ready * Only called from render thread * @param RHICmdList Used to write any commands required to generate the VT page data * @param FeatureLevel The current RHI feature level * @param ProducerHandle Handle to this producer * @param LayerMask Mask of requested layers; can be used to only produce data for these layers as an optimization, or ignored if all layers are logically produced together * @param vLevel The mipmap level of the data * @param vAddress Bit-interleaved x,y page indexes * @param RequestHandle opaque handle returned from 'RequestPageData' * @param TargetLayers Array of 'FVTProduceTargetLayer' structs, gives location where each layer should write data * @return a 'IVirtualTextureFinalizer' which must be finalized to complete the operation */ virtual IVirtualTextureFinalizer* ProducePageData( FRHICommandListBase& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, EVTProducePageFlags Flags, const FVirtualTextureProducerHandle& ProducerHandle, uint8 LayerMask, uint8 vLevel, uint64 vAddress, uint64 RequestHandle, const FVTProduceTargetLayer* TargetLayers) = 0; /** Collect all task graph events. */ virtual void GatherProducePageDataTasks(FVirtualTextureProducerHandle const& ProducerHandle, FGraphEventArray& InOutTasks) const {} /** Collect all task graph events related to a request. */ virtual void GatherProducePageDataTasks(uint64 RequestHandle, FGraphEventArray& InOutTasks) const {}; /** Dump any type specific debug info. */ virtual void DumpToConsole(bool verbose) {} /** Called on every virtual texture system update once all requests are completed, if bNotifyCompleted is enabled. */ virtual void OnRequestsCompleted() {} }; enum class EVTPageTableFormat : uint8 { UInt16, UInt32, }; /** * This interface represents a chunk of VT data allocated and owned by the renderer module, backed by both a page table texture, and a physical texture cache for each layer. * Both page table and physical texture may be shared amongst many different allocated virtual textures. * Any method that deals with physical texture requires an explicit LayerIndex parameter to identify the physical texture in question, * methods that don't have LayerIndex parameter refer to properties shared by all textures using the given page table * These are created with IRendererModule::AllocateVirtualTexture, and destroyed with IRendererModule::DestroyVirtualTexture * They must be allocated from render thread, but may be destroyed from any thread */ class IAllocatedVirtualTexture { public: static const uint32 LayersPerPageTableTexture = 4u; inline IAllocatedVirtualTexture(const FAllocatedVTDescription& InDesc, uint32 InBlockWidthInTiles, uint32 InBlockHeightInTiles, uint32 InWidthInBlocks, uint32 InHeightInBlocks, uint32 InDepthInTiles) : Description(InDesc) , BlockWidthInTiles(InBlockWidthInTiles) , BlockHeightInTiles(InBlockHeightInTiles) , WidthInBlocks(InWidthInBlocks) , HeightInBlocks(InHeightInBlocks) , DepthInTiles(InDepthInTiles) , FrameDeleted(0u) , NumRefs(0) , bIsWaitingToMap(false) , PageTableFormat(EVTPageTableFormat::UInt32) , SpaceID(~0u) , MaxLevel(0u) , VirtualAddress(~0u) , VirtualPageX(~0u) , VirtualPageY(~0u) {} virtual uint32 GetPersistentHash() const = 0; virtual uint32 GetNumPageTableTextures() const = 0; virtual FRHITexture* GetPageTableTexture(uint32 InPageTableIndex) const = 0; virtual FRHITexture* GetPageTableIndirectionTexture() const = 0; virtual uint32 GetPhysicalTextureSize(uint32 InLayerIndex) const = 0; virtual FRHITexture* GetPhysicalTexture(uint32 InLayerIndex) const = 0; virtual FRHIShaderResourceView* GetPhysicalTextureSRV(uint32 InLayerIndex, bool bSRGB) const = 0; /** Writes 2x FUintVector4 */ virtual void GetPackedPageTableUniform(FUintVector4* OutUniform) const = 0; /** Writes 1x FUintVector4 */ virtual void GetPackedUniform(FUintVector4* OutUniform, uint32 LayerIndex) const = 0; virtual void DumpToConsole(bool bVerbose) const {} inline const FAllocatedVTDescription& GetDescription() const { return Description; } inline const FVirtualTextureProducerHandle& GetProducerHandle(uint32 InLayerIndex) const { check(InLayerIndex < Description.NumTextureLayers); return Description.ProducerHandle[InLayerIndex]; } inline uint32 GetVirtualTileSize() const { return Description.TileSize; } inline uint32 GetTileBorderSize() const { return Description.TileBorderSize; } inline uint32 GetPhysicalTileSize() const { return Description.TileSize + Description.TileBorderSize * 2u; } inline uint32 GetNumTextureLayers() const { return Description.NumTextureLayers; } inline uint8 GetDimensions() const { return Description.Dimensions; } inline uint32 GetWidthInBlocks() const { return WidthInBlocks; } inline uint32 GetHeightInBlocks() const { return HeightInBlocks; } inline uint32 GetBlockWidthInTiles() const { return BlockWidthInTiles; } inline uint32 GetBlockHeightInTiles() const { return BlockHeightInTiles; } inline uint32 GetWidthInTiles() const { return BlockWidthInTiles * WidthInBlocks; } inline uint32 GetHeightInTiles() const { return BlockHeightInTiles * HeightInBlocks; } inline uint32 GetDepthInTiles() const { return DepthInTiles; } inline uint32 GetWidthInPixels() const { return GetWidthInTiles() * Description.TileSize; } inline uint32 GetHeightInPixels() const { return GetHeightInTiles() * Description.TileSize; } inline uint32 GetDepthInPixels() const { return DepthInTiles * Description.TileSize; } inline uint32 GetSpaceID() const { return SpaceID; } inline uint32 GetVirtualAddress() const { return VirtualAddress; } inline uint32 GetVirtualPageX() const { return VirtualPageX; } inline uint32 GetVirtualPageY() const { return VirtualPageY; } inline uint32 GetMaxLevel() const { return MaxLevel; } inline EVTPageTableFormat GetPageTableFormat() const { return PageTableFormat; } protected: friend class FVirtualTextureSystem; virtual void Destroy(class FVirtualTextureSystem* InSystem) = 0; virtual bool TryMapLockedTiles(class FVirtualTextureSystem* InSystem) const = 0; virtual ~IAllocatedVirtualTexture() {} FAllocatedVTDescription Description; uint32 BlockWidthInTiles; uint32 BlockHeightInTiles; uint32 WidthInBlocks; uint32 HeightInBlocks; uint32 DepthInTiles; uint32 FrameDeleted; int32 NumRefs; bool bIsWaitingToMap; // should be set explicitly by derived class constructor EVTPageTableFormat PageTableFormat; uint32 SpaceID; uint32 MaxLevel; uint32 VirtualAddress; uint32 VirtualPageX; uint32 VirtualPageY; }; /** * Interface for adaptive virtual textures. * This manages multiple allocated virtual textures in a space to simulate a single larger virtual texture. */ class IAdaptiveVirtualTexture { public: /** Get the persistent allocated virtual texture for low mips from the adaptive virtual texture. */ virtual IAllocatedVirtualTexture* GetAllocatedVirtualTexture() = 0; protected: friend class FVirtualTextureSystem; virtual ~IAdaptiveVirtualTexture() {} virtual int32 GetSpaceID() const = 0; virtual void Destroy(class FVirtualTextureSystem* InSystem) = 0; }; /** Describes an adaptive virtual texture. */ struct FAdaptiveVTDescription { uint32 TileCountX; uint32 TileCountY; uint32 MaxAdaptiveLevel; }; /** * Identifies a VT tile within a given producer */ union FVirtualTextureLocalTile { inline FVirtualTextureLocalTile() {} explicit inline FVirtualTextureLocalTile(const FVirtualTextureProducerHandle& InProducerHandle, uint32 InLocal_vAddress, uint8 InLocal_vLevel) : PackedProducerHandle(InProducerHandle.PackedValue) , Local_vAddress(InLocal_vAddress) , Local_vLevel(InLocal_vLevel) , Pad(0) { } explicit inline FVirtualTextureLocalTile(uint64 InPackedValue) : PackedValue(InPackedValue) { Pad = 0; } inline FVirtualTextureProducerHandle GetProducerHandle() const { return FVirtualTextureProducerHandle(PackedProducerHandle); } uint64 PackedValue = 0; struct { // Core "identity" data that identify a given tile uint32 PackedProducerHandle; uint32 Local_vAddress : 24; uint32 Local_vLevel : 4; uint32 Pad : 4; // Important : this has to be kept in sync with FVirtualTextureLocalTileRequest : both are packed in a uint64 and the latter stores additional info in these bits }; friend inline bool operator==(const FVirtualTextureLocalTile& Lhs, const FVirtualTextureLocalTile& Rhs) { return Lhs.PackedValue == Rhs.PackedValue; } friend inline bool operator!=(const FVirtualTextureLocalTile& Lhs, const FVirtualTextureLocalTile& Rhs) { return Lhs.PackedValue != Rhs.PackedValue; } friend inline uint32 GetTypeHash(const FVirtualTextureLocalTile& T) { return GetTypeHash(T.PackedValue); } inline uint16 GetMurmurHash() const { return static_cast(MurmurFinalize64(PackedValue)); } }; static_assert(sizeof(FVirtualTextureLocalTile) == sizeof(uint64), "Bad packing"); /** * Identifies a VT tile request : a VT tile, along with priority information for processing this request */ union FVirtualTextureLocalTileRequest { explicit inline FVirtualTextureLocalTileRequest() {} explicit inline FVirtualTextureLocalTileRequest(const FVirtualTextureLocalTile& InTile, EVTProducerPriority InProducerPriority, EVTInvalidatePriority InInvalidatePriority) : TilePackedValue(InTile.PackedValue) , ProducerPriority(static_cast(InProducerPriority)) , InvalidatePriority(static_cast(InInvalidatePriority)) { checkSlow(InTile == GetTile()); ValidatePriorities(); } inline FVirtualTextureLocalTile GetTile() const { return FVirtualTextureLocalTile(TilePackedValue); } inline EVTProducerPriority GetProducerPriority() const { return static_cast(ProducerPriority); } inline EVTInvalidatePriority GetInvalidatePriority() const { return static_cast(InvalidatePriority); } inline FVirtualTextureLocalTileRequest& MergeWith(const FVirtualTextureLocalTileRequest& InOther) { // Keep the highest priorities of the 2 : ProducerPriority = FMath::Max(ProducerPriority, InOther.ProducerPriority); InvalidatePriority = FMath::Max(InvalidatePriority, InOther.InvalidatePriority); checkSlow(*this == InOther); checkSlow(GetTile() == InOther.GetTile()); ValidatePriorities(); return *this; } uint64 PackedValue; struct { // Core "identity" data that identifies a given tile : uint64 TilePackedValue : 60; // Additional data about the request : uint64 ProducerPriority : 3; // See EVTProducerPriority uint64 InvalidatePriority : 1; // Indicates whether the tile should get prioritized over others }; friend inline bool operator==(const FVirtualTextureLocalTileRequest& Lhs, const FVirtualTextureLocalTileRequest& Rhs) { return Lhs.GetIdentity() == Rhs.GetIdentity(); } friend inline bool operator!=(const FVirtualTextureLocalTileRequest& Lhs, const FVirtualTextureLocalTileRequest& Rhs) { return Lhs.GetIdentity() != Rhs.GetIdentity(); } friend inline uint32 GetTypeHash(const FVirtualTextureLocalTileRequest& T) { return GetTypeHash(T.GetIdentity()); } inline uint64 GetIdentity() const { return TilePackedValue; } // 2 requests from the same tile are considered identical (it's up to the user to "merge" them if necessary) inline uint16 GetMurmurHash() const { return static_cast(MurmurFinalize64(GetIdentity())); } private: RENDERCORE_API void ValidatePriorities() const; }; static_assert(sizeof(FVirtualTextureLocalTileRequest) == sizeof(uint64), "Bad packing"); /** * Helper function that tries to add a new FVirtualTextureLocalTileRequest to a TSet but, in case this tile is already in the set, merges it with the existing request : */ inline FVirtualTextureLocalTileRequest& AddOrMergeTileRequest(const FVirtualTextureLocalTileRequest& InTileRequest, TSet& TileRequests) { bool bAlreadyInSet = false; FVirtualTextureLocalTileRequest& Result = TileRequests.FindOrAdd(InTileRequest, &bAlreadyInSet); if (bAlreadyInSet) { // If the request already exists, merge with this new request, to keep the highest priority : Result.MergeWith(InTileRequest); } return Result; } RENDERCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogVirtualTexturing, Log, All); DECLARE_STATS_GROUP(TEXT("Virtual Texturing"), STATGROUP_VirtualTexturing, STATCAT_Advanced); DECLARE_STATS_GROUP(TEXT("Virtual Texture Memory"), STATGROUP_VirtualTextureMemory, STATCAT_Advanced);