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

462 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LandscapeEditReadback.h"
#include "Engine/Texture2D.h"
#include "LandscapePrivate.h"
#include "RenderingThread.h"
#include "RenderUtils.h"
#include "Hash/CityHashHelpers.h"
namespace UE::Landscape::Private
{
static TAutoConsoleVariable<int32> CVarReadbackPoolSizeMB(
TEXT("landscape.ReadbackPoolSizeMB"),
256,
TEXT("Minimum pool size (in MB) for the editor readbacks. This ensures a minimum amount of readback textures are left in the pool when reclaiming memory, which avoids severe hiccups when reallocating a lot of resources."));
static int32 TotalStagingTexturesAllocatedSize = 0;
}
/** Data for a read back task. */
struct FLandscapeEditReadbackTaskImpl
{
/** Completion state for a read back task. */
enum class ECompletionState : uint64
{
None = 0, // Copy not submitted
Pending = 1, // Copy submitted, waiting for GPU
Complete = 2 // Result copied back from GPU
};
// Create on game thread
FTextureResource const* TextureResource = nullptr;
FLandscapeEditLayerReadback::FReadbackContext ReadbackContext;
uint32 InitFrameId = 0;
FIntPoint Size = FIntPoint(ForceInitToZero);
uint32 NumMips = 0;
EPixelFormat Format = PF_Unknown;
int32 StagingTexturesAllocatedSize = 0;
// Create on render thread
TArray<FTextureRHIRef> StagingTextures;
FGPUFenceRHIRef ReadbackFence;
// Result written on render thread and read on game thread
ECompletionState CompletionState = ECompletionState::None;
TArray<TArray<FColor>> Result;
};
/** Initialize the read back task data that is written by game thread. */
void InitTask_GameThread(FLandscapeEditReadbackTaskImpl& Task, UTexture2D const* InTexture, FLandscapeEditLayerReadback::FReadbackContext&& InReadbackContext, uint32 InFrameId)
{
Task.TextureResource = InTexture->GetResource();
Task.ReadbackContext = MoveTemp(InReadbackContext);
Task.InitFrameId = InFrameId;
Task.Size = FIntPoint(InTexture->GetSizeX(), InTexture->GetSizeY());
Task.NumMips = InTexture->GetNumMips();
Task.Format = InTexture->GetPixelFormat();
Task.CompletionState = FLandscapeEditReadbackTaskImpl::ECompletionState::None;
}
/** Initialize the read back task resources. */
bool InitTask_RenderThread(FLandscapeEditReadbackTaskImpl& Task)
{
using namespace UE::Landscape::Private;
if (Task.StagingTextures.Num() == 0 || !Task.StagingTextures[0].IsValid() || Task.StagingTextures[0]->GetSizeXYZ() != FIntVector(Task.Size.X, Task.Size.Y, 1) || (Task.StagingTextures[0]->GetFormat() != Task.Format))
{
Task.StagingTextures.SetNum(Task.NumMips);
for (uint32 MipIndex = 0; MipIndex < Task.NumMips; ++MipIndex)
{
const int32 MipWidth = FMath::Max(Task.Size.X >> MipIndex, 1);
const int32 MipHeight = FMath::Max(Task.Size.Y >> MipIndex, 1);
const FRHITextureCreateDesc Desc =
FRHITextureCreateDesc::Create2D(TEXT("LandscapeEditReadbackTask"), MipWidth, MipHeight, Task.Format)
.SetFlags(ETextureCreateFlags::CPUReadback);
Task.StagingTextures[MipIndex] = RHICreateTexture(Desc);
}
Task.StagingTexturesAllocatedSize += CalcTextureSize(Task.Size.X, Task.Size.Y, Task.Format, Task.NumMips);
TotalStagingTexturesAllocatedSize += Task.StagingTexturesAllocatedSize;
}
if (!Task.ReadbackFence.IsValid())
{
Task.ReadbackFence = RHICreateGPUFence(TEXT("LandscapeEditReadbackTask"));
}
Task.ReadbackFence->Clear();
return true;
}
/** Kick the GPU work for the read back task. */
void KickTask_RenderThread(FRHICommandListImmediate& RHICmdList, FLandscapeEditReadbackTaskImpl& Task)
{
// Transition staging textures for write.
TArray <FRHITransitionInfo> Transitions;
Transitions.Add(FRHITransitionInfo(Task.TextureResource->GetTexture2DRHI(), ERHIAccess::SRVMask, ERHIAccess::CopySrc));
for (uint32 MipIndex = 0; MipIndex < Task.NumMips; ++MipIndex)
{
Transitions.Add(FRHITransitionInfo(Task.StagingTextures[MipIndex], ERHIAccess::Unknown, ERHIAccess::CopyDest));
}
RHICmdList.Transition(Transitions);
// Copy to staging textures.
for (uint32 MipIndex = 0; MipIndex < Task.NumMips; ++MipIndex)
{
const int32 MipWidth = FMath::Max(Task.Size.X >> MipIndex, 1);
const int32 MipHeight = FMath::Max(Task.Size.Y >> MipIndex, 1);
FRHICopyTextureInfo Info;
Info.Size = FIntVector(MipWidth, MipHeight, 1);
Info.SourceMipIndex = MipIndex;
RHICmdList.CopyTexture(Task.TextureResource->GetTexture2DRHI(), Task.StagingTextures[MipIndex], Info);
}
// Transition staging textures for read.
Transitions.Reset();
Transitions.Add(FRHITransitionInfo(Task.TextureResource->GetTexture2DRHI(), ERHIAccess::CopySrc, ERHIAccess::SRVMask));
for (uint32 MipIndex = 0; MipIndex < Task.NumMips; ++MipIndex)
{
Transitions.Add(FRHITransitionInfo(Task.StagingTextures[MipIndex], ERHIAccess::Unknown, ERHIAccess::CPURead));
}
RHICmdList.Transition(Transitions);
// Write fence used to read back without stalling.
RHICmdList.WriteGPUFence(Task.ReadbackFence);
Task.CompletionState = FLandscapeEditReadbackTaskImpl::ECompletionState::Pending;
}
/**
* Update the read back task on the render thread. Check if the GPU work is complete and if it is copy the data.
* @return true if the task's state is Complete, false if it is still Pending :
*/
bool UpdateTask_RenderThread(FRHICommandListImmediate& RHICmdList, FLandscapeEditReadbackTaskImpl& Task, bool bFlush)
{
if (Task.CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Pending && (bFlush || Task.ReadbackFence->Poll()))
{
// Read back to Task.Result
Task.Result.SetNum(Task.NumMips);
for (uint32 MipIndex = 0; MipIndex < Task.NumMips; ++MipIndex)
{
const int32 MipWidth = FMath::Max(Task.Size.X >> MipIndex, 1);
const int32 MipHeight = FMath::Max(Task.Size.Y >> MipIndex, 1);
// Editor always runs on GPU zero
const uint32 GPUIndex = 0;
Task.Result[MipIndex].SetNum(MipWidth * MipHeight);
void* Data = nullptr;
int32 TargetWidth, TargetHeight;
RHICmdList.MapStagingSurface(Task.StagingTextures[MipIndex], Task.ReadbackFence.GetReference(), Data, TargetWidth, TargetHeight, GPUIndex);
check(Data != nullptr);
check(MipWidth <= TargetWidth && MipHeight <= TargetHeight);
FColor const* ReadPtr = (FColor*)Data;
FColor* WritePtr = Task.Result[MipIndex].GetData();
for (int32 Y = 0; Y < MipHeight; ++Y)
{
FMemory::Memcpy(WritePtr, ReadPtr, MipWidth * sizeof(FColor));
ReadPtr += TargetWidth;
WritePtr += MipWidth;
}
RHICmdList.UnmapStagingSurface(Task.StagingTextures[MipIndex], GPUIndex);
}
// Write completion flag for game thread.
FPlatformMisc::MemoryBarrier();
Task.CompletionState = FLandscapeEditReadbackTaskImpl::ECompletionState::Complete;
}
return (Task.CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Complete);
}
/**
* Pool of read back tasks.
* Decouples task ownership so that that tasks can be easily released and recycled.
*/
class FLandscapeEditReadbackTaskPool : public FRenderResource
{
public:
/** Pool uses chunked array to avoid task data being moved by a realloc. */
TChunkedArray< FLandscapeEditReadbackTaskImpl > Pool;
/** Allocation count used to check if there are any tasks to Tick. */
uint32 AllocCount = 0;
/** Frame count used to validate and garbage collect. */
uint32 FrameCount = 0;
void ReleaseRHI() override
{
Pool.Empty();
}
/** Allocate task data from the pool. */
int32 Allocate(UTexture2D const* InTexture, FLandscapeEditLayerReadback::FReadbackContext&& InReadbackContext)
{
int32 CurrentIndex = 0;
int32 BestEntryIndex = INDEX_NONE;
FIntVector TextureSize(InTexture->GetSizeX(), InTexture->GetSizeY(), 1);
auto ItEnd = Pool.end();
for (auto It = Pool.begin(); It != ItEnd; ++It, ++CurrentIndex)
{
FLandscapeEditReadbackTaskImpl& Task = *It;
// If the entry is unused, it's a candidate
if (Task.TextureResource == nullptr)
{
BestEntryIndex = CurrentIndex;
// Check the entry's texture size to ensure it's the best possible candidate. If so, no need to look further :
if (!Task.StagingTextures.IsEmpty() && Task.StagingTextures[0].IsValid() && (Task.StagingTextures[0]->GetSizeXYZ() == TextureSize) && (Task.Format == InTexture->GetPixelFormat()))
{
break;
}
}
}
if (BestEntryIndex == INDEX_NONE)
{
Pool.Add();
BestEntryIndex = Pool.Num() - 1;
}
InitTask_GameThread(Pool[BestEntryIndex], InTexture, MoveTemp(InReadbackContext), FrameCount);
++AllocCount;
return BestEntryIndex;
}
/** Return task data to the pool. */
void Free(int32 InTaskHandle)
{
check(InTaskHandle != -1);
check(AllocCount > 0);
AllocCount --;
// Submit render thread command to mark pooled task as free.
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_Free)([Task = &Pool[InTaskHandle]](FRHICommandListImmediate& RHICmdList)
{
Task->TextureResource = nullptr;
});
}
/** Free render resources that have been unused for long enough. */
void GarbageCollect()
{
using namespace UE::Landscape::Private;
const uint32 PoolSize = Pool.Num();
if (PoolSize > 0)
{
// Garbage collect a maximum of one item per call to reduce overhead if pool has grown large.
FLandscapeEditReadbackTaskImpl* Task = &Pool[FrameCount % PoolSize];
if (Task->InitFrameId + 100 < FrameCount)
{
if (Task->TextureResource != nullptr)
{
// Task not completed after 100 updates. We are probably leaking tasks!
UE_LOG(LogLandscape, Warning, TEXT("Leaking landscape edit layer read back tasks."))
}
else
{
// Free data allocations
Task->ReadbackContext.Empty();
Task->Result.Empty();
const int32 MinPoolSize = CVarReadbackPoolSizeMB.GetValueOnGameThread();
const int32 MinPoolSizeBytes = MinPoolSize * 1024 * 1024;
if ((!Task->StagingTextures.IsEmpty() || Task->ReadbackFence.IsValid())
&& ((TotalStagingTexturesAllocatedSize - Task->StagingTexturesAllocatedSize) > MinPoolSizeBytes)) // Don't deplete the pool under the minimum limit
{
TotalStagingTexturesAllocatedSize -= Task->StagingTexturesAllocatedSize;
check(TotalStagingTexturesAllocatedSize >= 0);
// Release the render resources (which may already be released)
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_Release)([Task](FRHICommandListImmediate& RHICmdList)
{
Task->StagingTextures.Reset();
Task->ReadbackFence.SafeRelease();
});
}
}
}
}
FrameCount++;
}
void FlushAll()
{
// Flush all pending tasks in a single command
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_FlushAll)([this](FRHICommandListImmediate& RHICmdList)
{
auto ItEnd = Pool.end();
for (auto It = Pool.begin(); It != ItEnd; ++It)
{
FLandscapeEditReadbackTaskImpl& Task = *It;
if (Task.TextureResource != nullptr)
{
bool bTaskComplete = UpdateTask_RenderThread(RHICmdList, Task, /*bFlush = */true);
check(bTaskComplete); // Flush should never fail to complete
}
}
});
TRACE_CPUPROFILER_EVENT_SCOPE(LandscapeLayers_ReadbackFlushAll);
FlushRenderingCommands();
}
};
/** Static global pool object. */
static TGlobalResource< FLandscapeEditReadbackTaskPool > GReadbackTaskPool;
FLandscapeEditLayerReadback::FLandscapeEditLayerReadback()
{}
FLandscapeEditLayerReadback::~FLandscapeEditLayerReadback()
{
for (int32 TaskHandle : TaskHandles)
{
GReadbackTaskPool.Free(TaskHandle);
}
ensure(!bLastReadbackWasIntermediate); // Expecting intermediate render data to always be cleaned up by a regular render. It shouldn't escape the lifetime of this object.
}
bool FLandscapeEditLayerReadback::SetHash(uint64 InHash)
{
const bool bChanged = InHash != Hash;
Hash = InHash;
return bChanged;
}
void FLandscapeEditLayerReadback::SetLastReadbackWasIntermediate(bool bValue)
{
bLastReadbackWasIntermediate = bValue;
}
bool FLandscapeEditLayerReadback::GetLastReadbackWasIntermediate() const
{
return bLastReadbackWasIntermediate;
}
void FLandscapeEditLayerReadback::Enqueue(UTexture2D const* InSourceTexture, FReadbackContext&& InReadbackContext)
{
const int32 TaskHandle = GReadbackTaskPool.Allocate(InSourceTexture, MoveTemp(InReadbackContext));
if (ensure(TaskHandle != -1))
{
TaskHandles.Add(TaskHandle);
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_Queue)([TaskHandle](FRHICommandListImmediate& RHICmdList)
{
InitTask_RenderThread(GReadbackTaskPool.Pool[TaskHandle]);
KickTask_RenderThread(RHICmdList, GReadbackTaskPool.Pool[TaskHandle]);
});
}
}
void FLandscapeEditLayerReadback::Tick()
{
TArray<int32> TaskHandlesCopy(TaskHandles);
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_Tick)([TasksToUpdate = MoveTemp(TaskHandlesCopy)](FRHICommandListImmediate& RHICmdList)
{
for (int32 TaskHandle : TasksToUpdate)
{
// Tick the task :
bool bTaskComplete = UpdateTask_RenderThread(RHICmdList, GReadbackTaskPool.Pool[TaskHandle], false);
// Stop processing at the first incomplete task in order not to get a task's state to Complete before a one of its previous task (in case their GPU fences are written in between the calls to UpdateTask_RenderThread) :
if (!bTaskComplete)
{
break;
}
}
});
}
void FLandscapeEditLayerReadback::Flush()
{
TArray<int32> TaskHandlesCopy(TaskHandles);
ENQUEUE_RENDER_COMMAND(FLandscapeEditLayerReadback_Flush)([TasksToUpdate = MoveTemp(TaskHandlesCopy)](FRHICommandListImmediate& RHICmdList)
{
for (int32 TaskHandle : TasksToUpdate)
{
bool bTaskComplete = UpdateTask_RenderThread(RHICmdList, GReadbackTaskPool.Pool[TaskHandle], true);
check(bTaskComplete); // Flush should never fail to complete
}
});
TRACE_CPUPROFILER_EVENT_SCOPE(LandscapeLayers_ReadbackFlush);
FlushRenderingCommands();
}
int32 FLandscapeEditLayerReadback::GetCompletedResultNum() const
{
// Find last task marked as complete. We can assume that tasks complete in order.
for (int32 TaskIndex = TaskHandles.Num() - 1; TaskIndex >= 0; --TaskIndex)
{
if (GReadbackTaskPool.Pool[TaskHandles[TaskIndex]].CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Complete)
{
return TaskIndex + 1;
}
}
return 0;
}
TArray<TArray<FColor>> const& FLandscapeEditLayerReadback::GetResult(int32 InResultIndex) const
{
check(InResultIndex >= 0);
check(InResultIndex < TaskHandles.Num());
check(GReadbackTaskPool.Pool[TaskHandles[InResultIndex]].CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Complete);
return GReadbackTaskPool.Pool[TaskHandles[InResultIndex]].Result;
}
FLandscapeEditLayerReadback::FReadbackContext const& FLandscapeEditLayerReadback::GetResultContext(int32 InResultIndex) const
{
check(InResultIndex >= 0);
check(InResultIndex < TaskHandles.Num());
check(GReadbackTaskPool.Pool[TaskHandles[InResultIndex]].CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Complete);
return GReadbackTaskPool.Pool[TaskHandles[InResultIndex]].ReadbackContext;
}
void FLandscapeEditLayerReadback::ReleaseCompletedResults(int32 InResultNum)
{
check(InResultNum > 0);
check(InResultNum <= TaskHandles.Num());
check(GReadbackTaskPool.Pool[TaskHandles[InResultNum - 1]].CompletionState == FLandscapeEditReadbackTaskImpl::ECompletionState::Complete);
for (int32 TaskIndex = 0; TaskIndex < InResultNum; ++TaskIndex)
{
GReadbackTaskPool.Free(TaskHandles[TaskIndex]);
}
TaskHandles.RemoveAt(0, InResultNum, EAllowShrinking::No);
}
bool FLandscapeEditLayerReadback::HasWork()
{
return GReadbackTaskPool.AllocCount > 0;
}
void FLandscapeEditLayerReadback::GarbageCollectTasks()
{
GReadbackTaskPool.GarbageCollect();
}
void FLandscapeEditLayerReadback::FlushAllReadbackTasks()
{
GReadbackTaskPool.FlushAll();
}