帧率优化, GPU数据读操作异步

This commit is contained in:
顾宏 2026-03-06 18:25:27 +08:00
parent c8aa3652ff
commit 5654428986
4 changed files with 216 additions and 60 deletions

View File

@ -11,6 +11,14 @@ ACameraCaptureActor::ACameraCaptureActor()
// PrimaryActorTick.bCanEverTick = false; // PrimaryActorTick.bCanEverTick = false;
} }
void ACameraCaptureActor::CaptureNow()
{
if (ViewCapture)
{
ViewCapture->CaptureScene();
}
}
void ACameraCaptureActor::BeginPlay() void ACameraCaptureActor::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
@ -25,8 +33,8 @@ void ACameraCaptureActor::PostEditChangeProperty(FPropertyChangedEvent& Property
// 任何关键参数变化都重建(避免 RT 尺寸/数量不匹配) // 任何关键参数变化都重建(避免 RT 尺寸/数量不匹配)
BuildViewCaptures(); BuildViewCaptures();
} }
#endif
#endif
void ACameraCaptureActor::BuildViewCaptures() void ACameraCaptureActor::BuildViewCaptures()
{ {
if (ViewCapture) if (ViewCapture)
@ -35,14 +43,10 @@ void ACameraCaptureActor::BuildViewCaptures()
ViewCapture = nullptr; ViewCapture = nullptr;
} }
if (ViewRT)
{
ViewRT = nullptr;
}
ViewRT = NewObject<UTextureRenderTarget2D>(this); ViewRT = NewObject<UTextureRenderTarget2D>(this);
ViewRT->RenderTargetFormat = RTF_RGBA8;
ViewRT->InitCustomFormat(ViewWidth, ViewHeight, PF_B8G8R8A8, true);
ViewRT->ClearColor = FLinearColor::Black; ViewRT->ClearColor = FLinearColor::Black;
ViewRT->InitAutoFormat(ViewWidth, ViewHeight);
ViewRT->UpdateResourceImmediate(true); ViewRT->UpdateResourceImmediate(true);
ViewCapture = NewObject<USceneCaptureComponent2D>(this, TEXT("ViewCapture")); ViewCapture = NewObject<USceneCaptureComponent2D>(this, TEXT("ViewCapture"));
@ -50,7 +54,8 @@ void ACameraCaptureActor::BuildViewCaptures()
ViewCapture->RegisterComponent(); ViewCapture->RegisterComponent();
ViewCapture->TextureTarget = ViewRT; ViewCapture->TextureTarget = ViewRT;
ViewCapture->CaptureSource = ESceneCaptureSource::SCS_FinalColorHDR; ViewCapture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
ViewCapture->bCaptureEveryFrame = false; ViewCapture->bCaptureEveryFrame = false;
ViewCapture->bCaptureOnMovement = false; ViewCapture->bCaptureOnMovement = false;

View File

@ -39,6 +39,7 @@ public:
USceneCaptureComponent2D* GetViewCapture() { return ViewCapture; } USceneCaptureComponent2D* GetViewCapture() { return ViewCapture; }
UTextureRenderTarget2D* GetViewRT() { return ViewRT; } UTextureRenderTarget2D* GetViewRT() { return ViewRT; }
void CaptureNow();
// ===== 多视图参数 ===== // ===== 多视图参数 =====

View File

@ -3,12 +3,31 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "RHIGPUReadback.h"
#include "RHICommandList.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Subsystems/WorldSubsystem.h" #include "Subsystems/WorldSubsystem.h"
#include "Tickable.h" #include "Tickable.h"
#include "CameraCaptureSubSystem.generated.h" #include "CameraCaptureSubSystem.generated.h"
struct FCameraReadbackItem
{
int32 CameraIndex = INDEX_NONE;
int32 Width = 0;
int32 Height = 0;
TUniquePtr<FRHIGPUTextureReadback> Readback;
};
struct FPendingAtlasCapture
{
int64 FrameId = 0;
int32 AtlasW = 0;
int32 AtlasH = 0;
TArray<FCameraReadbackItem> Items;
};
class ACameraCaptureActor; class ACameraCaptureActor;
@ -37,7 +56,13 @@ private:
void CaptureViewToImageTick(); void CaptureViewToImageTick();
void AsyncWriteImageToDisk(TArray<FColor>& AtlasPixels, const int32 AtlasW, const int32 AtlasH); // :发起 Capture + Enqueue GPU Readback
void BeginCaptureViewToAtlas();
// 轮询并取回 GPU 数据,再拼图
void TryResolveCaptureViewAtlas();
void AsyncWriteImageToDisk(TArray<FColor> AtlasPixels, const int32 AtlasW, const int32 AtlasH);
TArray<TWeakObjectPtr<ACameraCaptureActor>> AllCaptureCameras; TArray<TWeakObjectPtr<ACameraCaptureActor>> AllCaptureCameras;
@ -45,4 +70,7 @@ private:
UPROPERTY() UPROPERTY()
FString OutPutDirectoryPath; FString OutPutDirectoryPath;
TUniquePtr<FPendingAtlasCapture> PendingAtlasCapture;
bool bReadbackInFlight = false;
}; };

View File

@ -81,67 +81,189 @@ void UCameraCaptureSubSystem::CaptureViewToImageTick()
return; return;
} }
TArray<FColor> AtlasPixels; // 已经有一批在等 GPU 回读,就先尝试取回
if (bReadbackInFlight)
{
TryResolveCaptureViewAtlas();
return;
}
// 没有在飞行中的回读,就启动新的一批
BeginCaptureViewToAtlas();
}
void UCameraCaptureSubSystem::BeginCaptureViewToAtlas()
{
const int32 AtlasW = ViewWidth * AtlasGridX; const int32 AtlasW = ViewWidth * AtlasGridX;
const int32 AtlasH = ViewHeight * AtlasGridY; const int32 AtlasH = ViewHeight * AtlasGridY;
AtlasPixels.SetNumZeroed(AtlasW * AtlasH);
for (int32 CameraIndex = 0; CameraIndex < AllCaptureCameras.Num(); CameraIndex++) PendingAtlasCapture = MakeUnique<FPendingAtlasCapture>();
PendingAtlasCapture->FrameId = (int64)GFrameCounter;
PendingAtlasCapture->AtlasW = AtlasW;
PendingAtlasCapture->AtlasH = AtlasH;
const int32 MaxTiles = AtlasGridX * AtlasGridY;
const int32 CameraCount = FMath::Min(AllCaptureCameras.Num(), MaxTiles);
PendingAtlasCapture->Items.Reserve(CameraCount);
for (int32 CameraIndex = 0; CameraIndex < CameraCount; ++CameraIndex)
{ {
if (!AllCaptureCameras[CameraIndex].IsValid()) if (!AllCaptureCameras[CameraIndex].IsValid())
{ {
continue; continue;
} }
UTextureRenderTarget2D* RenderTarget = AllCaptureCameras[CameraIndex]->GetViewRT(); ACameraCaptureActor* CaptureActor = AllCaptureCameras[CameraIndex].Get();
if (!CaptureActor)
{
continue;
}
// 先让这台 SceneCapture 更新一次 RT
CaptureActor->CaptureNow(); // 你自己封装一个函数,内部调用 ViewCapture->CaptureScene()
UTextureRenderTarget2D* RenderTarget = CaptureActor->GetViewRT();
if (!RenderTarget) if (!RenderTarget)
{ {
continue; continue;
} }
FTextureRenderTargetResource* RenderTargetRes = RenderTarget->GameThread_GetRenderTargetResource(); FTextureRenderTargetResource* RTRes = RenderTarget->GameThread_GetRenderTargetResource();
if (!RenderTargetRes) if (!RTRes)
{ {
continue; continue;
} }
TArray<FColor> Pixels; FRHITexture* SourceTexture = RTRes->GetRenderTargetTexture();
Pixels.SetNumUninitialized(ViewWidth * ViewHeight); if (!SourceTexture)
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
ReadFlags.SetLinearToGamma(true);
if (!RenderTargetRes->ReadPixels(Pixels, ReadFlags)) continue;
FlipVertical(Pixels, ViewWidth, ViewHeight);
const int32 GX = CameraIndex % AtlasGridX;
const int32 GY = CameraIndex / AtlasGridX;
const int32 DstX0 = GX * ViewWidth;
const int32 DstY0 = GY * ViewHeight;
for (int32 y = 0; y < ViewHeight; ++y)
{ {
const int32 DstY = DstY0 + y; continue;
if (DstY < 0 || DstY >= AtlasH) continue; }
const int32 SrcRow = y * ViewWidth; FCameraReadbackItem Item;
const int32 DstRow = DstY * AtlasW; Item.CameraIndex = CameraIndex;
Item.Width = ViewWidth;
Item.Height = ViewHeight;
Item.Readback = MakeUnique<FRHIGPUTextureReadback>(
FName(*FString::Printf(TEXT("CameraReadback_%d"), CameraIndex))
);
for (int32 x = 0; x < ViewWidth; ++x) FRHIGPUTextureReadback* ReadbackPtr = Item.Readback.Get();
const FResolveRect CopyRect(0, 0, ViewWidth, ViewHeight);
ENQUEUE_RENDER_COMMAND(QueueCameraTextureReadback)(
[ReadbackPtr, SourceTexture, CopyRect](FRHICommandListImmediate& RHICmdList)
{ {
const int32 DstX = DstX0 + x; if (ReadbackPtr && SourceTexture)
if (DstX < 0 || DstX >= AtlasW) continue; {
ReadbackPtr->EnqueueCopy(RHICmdList, SourceTexture, CopyRect);
}
});
AtlasPixels[DstRow + DstX] = Pixels[SrcRow + x]; PendingAtlasCapture->Items.Add(MoveTemp(Item));
}
bReadbackInFlight = PendingAtlasCapture->Items.Num() > 0;
}
void UCameraCaptureSubSystem::TryResolveCaptureViewAtlas()
{
if (!bReadbackInFlight || !PendingAtlasCapture.IsValid())
{
return;
}
// 只要有一个还没准备好,就下一帧再来
for (const FCameraReadbackItem& Item : PendingAtlasCapture->Items)
{
if (!Item.Readback.IsValid() || !Item.Readback->IsReady())
{
return;
}
}
// 全部 ready开始组装 atlas
TArray<FColor> AtlasPixels;
AtlasPixels.SetNumZeroed(PendingAtlasCapture->AtlasW * PendingAtlasCapture->AtlasH);
for (const FCameraReadbackItem& Item : PendingAtlasCapture->Items)
{
if (!Item.Readback.IsValid())
{
continue;
}
int32 RowPitchInPixels = 0;
int32 BufferHeight = 0;
void* RawPtr = Item.Readback->Lock(RowPitchInPixels, &BufferHeight);
if (!RawPtr)
{
Item.Readback->Unlock();
continue;
}
// RawPtr 指向 GPU 读回后的 CPU 可访问数据
const FColor* SrcPixels = static_cast<const FColor*>(RawPtr);
// 先拷到一个连续的小图缓存里
TArray<FColor> ViewPixels;
ViewPixels.SetNumUninitialized(Item.Width * Item.Height);
for (int32 Y = 0; Y < Item.Height; ++Y)
{
const FColor* SrcRow = SrcPixels + Y * RowPitchInPixels;
FColor* DstRow = ViewPixels.GetData() + Y * Item.Width;
FMemory::Memcpy(DstRow, SrcRow, sizeof(FColor) * Item.Width);
}
Item.Readback->Unlock();
// 如果你最后导出的图上下颠倒,这里保留翻转
FlipVertical(ViewPixels, Item.Width, Item.Height);
const int32 GX = Item.CameraIndex % AtlasGridX;
const int32 GY = Item.CameraIndex / AtlasGridX;
const int32 DstX0 = GX * Item.Width;
const int32 DstY0 = GY * Item.Height;
for (int32 Y = 0; Y < Item.Height; ++Y)
{
const int32 AtlasY = DstY0 + Y;
if (AtlasY < 0 || AtlasY >= PendingAtlasCapture->AtlasH)
{
continue;
}
const int32 SrcRowIndex = Y * Item.Width;
const int32 DstRowIndex = AtlasY * PendingAtlasCapture->AtlasW;
for (int32 X = 0; X < Item.Width; ++X)
{
const int32 AtlasX = DstX0 + X;
if (AtlasX < 0 || AtlasX >= PendingAtlasCapture->AtlasW)
{
continue;
}
AtlasPixels[DstRowIndex + AtlasX] = ViewPixels[SrcRowIndex + X];
} }
} }
} }
AsyncWriteImageToDisk(AtlasPixels, AtlasW, AtlasH); const int32 AtlasW = PendingAtlasCapture->AtlasW;
const int32 AtlasH = PendingAtlasCapture->AtlasH;
PendingAtlasCapture.Reset();
bReadbackInFlight = false;
AsyncWriteImageToDisk(MoveTemp(AtlasPixels), AtlasW, AtlasH);
} }
void UCameraCaptureSubSystem::AsyncWriteImageToDisk(TArray<FColor>& AtlasPixels, const int32 AtlasW, const int32 AtlasH) void UCameraCaptureSubSystem::AsyncWriteImageToDisk(TArray<FColor> AtlasPixels, const int32 AtlasW, const int32 AtlasH)
{ {
const FString FullPath = FPaths::Combine( OutPutDirectoryPath, FString::Printf(TEXT("atlas_%010lld.png"), (int64)GFrameCounter) ); const FString FullPath = FPaths::Combine( OutPutDirectoryPath, FString::Printf(TEXT("atlas_%010lld.png"), (int64)GFrameCounter) );