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

1162 lines
33 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "VisualizeTexture.h"
#include "Misc/App.h"
#include "ShaderParameters.h"
#include "Misc/WildcardString.h"
#include "RHIStaticStates.h"
#include "RenderingThread.h"
#include "Shader.h"
#include "RenderTargetPool.h"
#include "GlobalShader.h"
#include "PipelineStateCache.h"
#include "PixelShaderUtils.h"
#include "Misc/FileHelper.h"
#include "RenderCore.h"
void FVisualizeTexture::ParseCommands(const TCHAR* Cmd, FOutputDevice &Ar)
{
#if SUPPORTS_VISUALIZE_TEXTURE
// Find out what command to do based on first parameter.
ECommand Command = ECommand::Unknown;
int32 ViewId = 0;
FString ViewName;
FString RDGResourceName;
TOptional<uint32> ResourceVersion;
TOptional<FWildcardString> ResourceListWildCard;
{
FString FirstParameter = FParse::Token(Cmd, 0);
if (FirstParameter.IsEmpty())
{
// NOP
}
else if (FChar::IsDigit(**FirstParameter))
{
Command = ECommand::DisableVisualization;
}
else if (FirstParameter == TEXT("help"))
{
Command = ECommand::DisplayHelp;
}
else if (FirstParameter == TEXT("pool"))
{
Command = ECommand::DisplayPoolResourceList;
}
else if (FirstParameter.StartsWith(TEXT("view=")))
{
if (FirstParameter.Len() == 5)
{
// Empty payload, reset view ID to zero
Command = ECommand::SetViewId;
ViewId = 0;
}
else if (FirstParameter[5] == TEXT('?'))
{
Command = ECommand::DisplayViewList;
}
else
{
Command = ECommand::SetViewId;
// Supports view ID or string name of view
TCHAR* EndOfNumber;
ViewId = FCString::Strtoi(&FirstParameter[5], &EndOfNumber, 0);
if (EndOfNumber - &FirstParameter[0] != FirstParameter.Len())
{
// Didn't parse as a number, treat it as a string
ViewId = 0;
ViewName = FirstParameter.Right(FirstParameter.Len() - 5);
}
}
}
else
{
const TCHAR* AfterAt = *FirstParameter;
while (*AfterAt != 0 && *AfterAt != TCHAR('@'))
{
++AfterAt;
}
if (*AfterAt == TCHAR('@'))
{
RDGResourceName = FirstParameter.Left(AfterAt - *FirstParameter);
ResourceVersion = FCString::Atoi(AfterAt + 1);
}
else
{
RDGResourceName = FirstParameter;
}
if (RDGResourceName.Contains(TEXT("*")))
{
ResourceListWildCard = FWildcardString(RDGResourceName);
Command = ECommand::DisplayResourceList;
}
else
{
Command = ECommand::VisualizeResource;
Visualize(RDGResourceName, ResourceVersion);
}
}
}
if (Command == ECommand::Unknown)
{
FVisualizeTexture::DisplayHelp(Ar);
DisplayResourceListToLog(TOptional<FWildcardString>());
}
else if (Command == ECommand::DisplayHelp)
{
FVisualizeTexture::DisplayHelp(Ar);
}
else if (Command == ECommand::DisableVisualization)
{
Visualize({});
}
else if (Command == ECommand::VisualizeResource)
{
Config = {};
Visualize(RDGResourceName, ResourceVersion);
for (;;)
{
FString Parameter = FParse::Token(Cmd, 0);
if (Parameter.IsEmpty())
{
break;
}
else if (Parameter == TEXT("uv0"))
{
Config.InputUVMapping = EInputUVMapping::LeftTop;
}
else if (Parameter == TEXT("uv1"))
{
Config.InputUVMapping = EInputUVMapping::Whole;
}
else if (Parameter == TEXT("uv2"))
{
Config.InputUVMapping = EInputUVMapping::PixelPerfectCenter;
}
else if (Parameter == TEXT("pip"))
{
Config.InputUVMapping = EInputUVMapping::PictureInPicture;
}
else if (Parameter == TEXT("bmp"))
{
Config.Flags |= EFlags::SaveBitmap;
}
else if (Parameter == TEXT("stencil"))
{
Config.Flags |= EFlags::SaveBitmapAsStencil;
}
else if (Parameter == TEXT("frac"))
{
Config.ShaderOp = EShaderOp::Frac;
}
else if (Parameter == TEXT("sat"))
{
Config.ShaderOp = EShaderOp::Saturate;
}
else if (Parameter.Left(3) == TEXT("mip"))
{
Parameter.RightInline(Parameter.Len() - 3, EAllowShrinking::No);
Config.MipIndex = FCString::Atoi(*Parameter);
}
else if (Parameter.Left(5) == TEXT("index"))
{
Parameter.RightInline(Parameter.Len() - 5, EAllowShrinking::No);
Config.ArrayIndex = FCString::Atoi(*Parameter);
}
// e.g. RGB*6, A, *22, /2.7, A*7
else if (Parameter.Left(3) == TEXT("rgb")
|| Parameter.Left(1) == TEXT("a")
|| Parameter.Left(1) == TEXT("r")
|| Parameter.Left(1) == TEXT("g")
|| Parameter.Left(1) == TEXT("b")
|| Parameter.Left(1) == TEXT("*")
|| Parameter.Left(1) == TEXT("/"))
{
Config.SingleChannel = -1;
if (Parameter.Left(3) == TEXT("rgb"))
{
Parameter.RightInline(Parameter.Len() - 3, EAllowShrinking::No);
}
else if (Parameter.Left(1) == TEXT("r")) Config.SingleChannel = 0;
else if (Parameter.Left(1) == TEXT("g")) Config.SingleChannel = 1;
else if (Parameter.Left(1) == TEXT("b")) Config.SingleChannel = 2;
else if (Parameter.Left(1) == TEXT("a")) Config.SingleChannel = 3;
if (Config.SingleChannel >= 0)
{
Parameter.RightInline(Parameter.Len() - 1, EAllowShrinking::No);
Config.SingleChannelMul = 1.0f;
Config.RGBMul = 0.0f;
}
float Mul = 1.0f;
// * or /
if (Parameter.Left(1) == TEXT("*"))
{
Parameter.RightInline(Parameter.Len() - 1, EAllowShrinking::No);
Mul = FCString::Atof(*Parameter);
}
else if (Parameter.Left(1) == TEXT("/"))
{
Parameter.RightInline(Parameter.Len() - 1, EAllowShrinking::No);
Mul = 1.0f / FCString::Atof(*Parameter);
}
Config.RGBMul *= Mul;
Config.SingleChannelMul *= Mul;
Config.AMul *= Mul;
}
else
{
Ar.Logf(TEXT("Error: parameter \"%s\" not recognized"), *Parameter);
}
}
}
else if (Command == ECommand::DisplayPoolResourceList)
{
ESortBy SortBy = ESortBy::Index;
for (;;)
{
FString Parameter = FParse::Token(Cmd, 0);
if (Parameter.IsEmpty())
{
break;
}
else if (Parameter == TEXT("byname"))
{
SortBy = ESortBy::Name;
}
else if (Parameter == TEXT("bysize"))
{
SortBy = ESortBy::Size;
}
else
{
Ar.Logf(TEXT("Error: parameter \"%s\" not recognized"), *Parameter);
}
}
DisplayPoolResourceListToLog(SortBy);
}
else if (Command == ECommand::DisplayResourceList)
{
bool bListAllocated = false;
ESortBy SortBy = ESortBy::Index;
for (;;)
{
FString Parameter = FParse::Token(Cmd, 0);
if (Parameter.IsEmpty())
{
break;
}
else
{
Ar.Logf(TEXT("Error: parameter \"%s\" not recognized"), *Parameter);
}
}
DisplayResourceListToLog(ResourceListWildCard);
}
else if (Command == ECommand::DisplayViewList)
{
DisplayViewListToLog();
}
else if (Command == ECommand::SetViewId) //-V547
{
Requested.ViewUniqueId = ViewId;
Requested.ViewName = ViewName;
}
else
{
unimplemented();
}
// Enable tracking when the system is first interacted with
if (State == EState::Inactive)
{
State = EState::TrackResources;
}
#endif
}
void FVisualizeTexture::DebugLogOnCrash()
{
#if SUPPORTS_VISUALIZE_TEXTURE
DisplayPoolResourceListToLog(ESortBy::Size);
DisplayResourceListToLog(TOptional<FWildcardString>());
#endif
}
void FVisualizeTexture::GetTextureInfos_GameThread(TArray<FString>& Infos) const
{
check(IsInGameThread());
FlushRenderingCommands();
for (uint32 Index = 0, Num = GRenderTargetPool.GetElementCount(); Index < Num; ++Index)
{
FPooledRenderTarget* RenderTarget = GRenderTargetPool.GetElementById(Index);
if (!RenderTarget)
{
continue;
}
FPooledRenderTargetDesc Desc = RenderTarget->GetDesc();
uint32 SizeInKB = (RenderTarget->ComputeMemorySize() + 1023) / 1024;
FString Entry = FString::Printf(TEXT("%s %d %s %d"),
*Desc.GenerateInfoString(),
Index + 1,
Desc.DebugName ? Desc.DebugName : TEXT("<Unnamed>"),
SizeInKB);
Infos.Add(Entry);
}
}
TGlobalResource<FVisualizeTexture> GVisualizeTexture;
#if SUPPORTS_VISUALIZE_TEXTURE
void FVisualizeTexture::DisplayHelp(FOutputDevice &Ar)
{
Ar.Logf(TEXT("VisualizeTexture/Vis <RDGResourceNameWildcard>:"));
Ar.Logf(TEXT(" Lists all RDG resource names with wildcard filtering."));
Ar.Logf(TEXT(""));
Ar.Logf(TEXT("VisualizeTexture/Vis <RDGResourceName>[@<Version>] [<Mode>] [PIP/UV0/UV1/UV2] [BMP] [FRAC/SAT] [FULL]:"));
Ar.Logf(TEXT(" RDGResourceName = Name of the resource set when creating it with RDG."));
Ar.Logf(TEXT(" Version = Integer to specify a specific intermediate version."));
Ar.Logf(TEXT(" Mode (examples):"));
Ar.Logf(TEXT(" RGB = RGB in range 0..1 (default)"));
Ar.Logf(TEXT(" *8 = RGB * 8"));
Ar.Logf(TEXT(" A = alpha channel in range 0..1"));
Ar.Logf(TEXT(" R = red channel in range 0..1"));
Ar.Logf(TEXT(" G = green channel in range 0..1"));
Ar.Logf(TEXT(" B = blue channel in range 0..1"));
Ar.Logf(TEXT(" A*16 = Alpha * 16"));
Ar.Logf(TEXT(" RGB/2 = RGB / 2"));
Ar.Logf(TEXT(" SubResource:"));
Ar.Logf(TEXT(" MIP5 = Mip level 5 (0 is default)"));
Ar.Logf(TEXT(" INDEX5 = Array Element 5 (0 is default)"));
Ar.Logf(TEXT(" InputMapping:"));
Ar.Logf(TEXT(" PIP = like UV1 but as picture in picture with normal rendering (default)"));
Ar.Logf(TEXT(" UV0 = UV in left top"));
Ar.Logf(TEXT(" UV1 = full texture"));
Ar.Logf(TEXT(" UV2 = pixel perfect centered"));
Ar.Logf(TEXT(" Flags:"));
Ar.Logf(TEXT(" BMP = save out bitmap to the screenshots folder (not on console, normalized)"));
Ar.Logf(TEXT(" STENCIL = Stencil normally displayed in alpha channel of depth. This option is used for BMP to get a stencil only BMP."));
Ar.Logf(TEXT(" FRAC = use frac() in shader (default)"));
Ar.Logf(TEXT(" SAT = use saturate() in shader"));
Ar.Logf(TEXT(""));
Ar.Logf(TEXT("VisualizeTexture/Vis 0"));
Ar.Logf(TEXT(" Stops visualizing a resource."));
Ar.Logf(TEXT(""));
Ar.Logf(TEXT("VisualizeTexture/Vis pool [BYNAME/BYSIZE]:"));
Ar.Logf(TEXT(" Shows list of all resources in the pool."));
Ar.Logf(TEXT(" BYNAME = sort pool list by name"));
Ar.Logf(TEXT(" BYSIZE = show pool list by size"));
Ar.Logf(TEXT(""));
Ar.Logf(TEXT("VisualizeTexture/Vis view=[ID/NAME]"));
Ar.Logf(TEXT(" Unique ID or name of view to visualize textures from, \"view=?\" to dump list of available views"));
Ar.Logf(TEXT(""));
}
void FVisualizeTexture::DisplayPoolResourceListToLog(FVisualizeTexture::ESortBy SortBy)
{
struct FSortedLines
{
FString Line;
int32 SortIndex = 0;
uint32 PoolIndex = 0;
bool operator < (const FSortedLines& B) const
{
// first large ones
if (SortIndex < B.SortIndex)
{
return true;
}
if (SortIndex > B.SortIndex)
{
return false;
}
return Line < B.Line;
}
};
TArray<FSortedLines> SortedLines;
for (uint32 Index = 0, Num = GRenderTargetPool.GetElementCount(); Index < Num; ++Index)
{
FPooledRenderTarget* RenderTarget = GRenderTargetPool.GetElementById(Index);
if (!RenderTarget)
{
continue;
}
const FPooledRenderTargetDesc Desc = RenderTarget->GetDesc();
const uint32 SizeInKB = (RenderTarget->ComputeMemorySize() + 1023) / 1024;
FString UnusedStr;
if (RenderTarget->GetUnusedForNFrames() > 0)
{
UnusedStr = FString::Printf(TEXT(" unused(%d)"), RenderTarget->GetUnusedForNFrames());
}
FSortedLines Element;
Element.PoolIndex = Index;
Element.SortIndex = Index;
FString InfoString = Desc.GenerateInfoString();
switch (SortBy)
{
case ESortBy::Index:
{
// Constant works well with the average name length
const uint32 TotalSpacerSize = 36;
const uint32 SpaceCount = FMath::Max<int32>(0, TotalSpacerSize - InfoString.Len());
for (uint32 Space = 0; Space < SpaceCount; ++Space)
{
InfoString.AppendChar((TCHAR)' ');
}
// Sort by index
Element.Line = FString::Printf(TEXT("%s %s %d KB%s"), *InfoString, Desc.DebugName, SizeInKB, *UnusedStr);
}
break;
case ESortBy::Name:
{
Element.Line = FString::Printf(TEXT("%s %s %d KB%s"), Desc.DebugName, *InfoString, SizeInKB, *UnusedStr);
Element.SortIndex = 0;
}
break;
case ESortBy::Size:
{
Element.Line = FString::Printf(TEXT("%d KB %s %s%s"), SizeInKB, *InfoString, Desc.DebugName, *UnusedStr);
Element.SortIndex = -(int32)SizeInKB;
}
break;
default:
checkNoEntry();
}
SortedLines.Add(Element);
}
SortedLines.Sort();
for (int32 Index = 0; Index < SortedLines.Num(); Index++)
{
const FSortedLines& Entry = SortedLines[Index];
UE_LOG(LogConsoleResponse, Log, TEXT(" %3d = %s"), Entry.PoolIndex + 1, *Entry.Line);
}
UE_LOG(LogConsoleResponse, Log, TEXT(""));
uint32 WholeCount;
uint32 WholePoolInKB;
uint32 UsedInKB;
GRenderTargetPool.GetStats(WholeCount, WholePoolInKB, UsedInKB);
UE_LOG(LogConsoleResponse, Log, TEXT("Pool: %d/%d MB (referenced/allocated)"), (UsedInKB + 1023) / 1024, (WholePoolInKB + 1023) / 1024);
UE_LOG(LogConsoleResponse, Log, TEXT(""));
}
void FVisualizeTexture::DisplayResourceListToLog(const TOptional<FWildcardString>& Wildcard)
{
if (!IsActive())
{
State = EState::DisplayResources;
DisplayResourcesParam = Wildcard;
return;
}
UE_LOG(LogConsoleResponse, Log, TEXT("RDGResourceName (what was rendered this frame, use <RDGResourceName>@<Version> to get intermediate versions):"));
TArray<FString> Entries;
Entries.Reserve(VersionCountMap.Num());
for (auto KV : VersionCountMap)
{
if (Wildcard.IsSet())
{
if (Wildcard->IsMatch(KV.Key))
{
Entries.Add(KV.Key);
}
}
else
{
Entries.Add(KV.Key);
}
}
Entries.Sort();
// Magic number works well with the name length we have
const int32 MaxColumnCount = 5;
const int32 SpaceBetweenColumns = 1;
const int32 TargetColumnHeight = 8;
int32 ColumnCount = FMath::Clamp(FMath::DivideAndRoundUp(Entries.Num(), TargetColumnHeight), 1, MaxColumnCount);
int32 ColumnHeight = FMath::DivideAndRoundUp(Entries.Num(), ColumnCount);
// Width of the column in characters, init with 0
TStaticArray<int32, MaxColumnCount> ColumnWidths;
for (int32 ColumnId = 0; ColumnId < MaxColumnCount; ColumnId++)
{
ColumnWidths[ColumnId] = 0;
}
for (int32 Index = 0, Count = Entries.Num(); Index < Count; ++Index)
{
const FString& Entry = *Entries[Index];
const int32 Column = Index / ColumnHeight;
ColumnWidths[Column] = FMath::Max(ColumnWidths[Column], Entry.Len());
}
// Print them sorted, if possible multiple in a line
for (int32 RowId = 0; RowId < ColumnHeight; RowId++)
{
FString Line;
int32 ColumnAlignment = 0;
for (int32 ColumnId = 0; ColumnId < ColumnCount; ColumnId++)
{
int32 EntryId = ColumnId * ColumnHeight + RowId;
if (EntryId >= Entries.Num())
{
break;
}
const FString& Entry = *Entries[EntryId];
const int32 SpaceCount = ColumnAlignment - Line.Len();
check(SpaceCount >= 0);
for (int32 Space = 0; Space < SpaceCount; ++Space)
{
Line.AppendChar((TCHAR)' ');
}
Line += Entry;
ColumnAlignment += SpaceBetweenColumns + ColumnWidths[ColumnId];
}
UE_LOG(LogConsoleResponse, Log, TEXT(" %s"), *Line);
}
UE_LOG(LogConsoleResponse, Log, TEXT(""));
}
void FVisualizeTexture::DisplayViewListToLog()
{
if (!IsActive())
{
State = EState::DisplayViews;
return;
}
// Display view list sorted by unique ID
TArray<FSetElementId> Entries;
Entries.Reserve(ViewDescriptionMap.Num());
for (auto ViewIt = ViewDescriptionMap.CreateConstIterator(); ViewIt; ++ViewIt)
{
Entries.Add(ViewIt.GetId());
}
Entries.Sort([ViewDescriptionMap = ViewDescriptionMap](const FSetElementId& A, const FSetElementId& B)
{
return ViewDescriptionMap.Get(A).Key < ViewDescriptionMap.Get(B).Key;
});
UE_LOG(LogConsoleResponse, Log, TEXT("Visualize Texture available views:"));
for (FSetElementId ElementId : Entries)
{
UE_LOG(LogConsoleResponse, Log, TEXT(" %d %s"), ViewDescriptionMap.Get(ElementId).Key, *ViewDescriptionMap.Get(ElementId).Value);
}
}
static TAutoConsoleVariable<int32> CVarAllowBlinking(
TEXT("r.VisualizeTexture.AllowBlinking"), 1,
TEXT("Whether to allow blinking when visualizing NaN or inf that can become irritating over time.\n"),
ECVF_RenderThreadSafe);
enum class EVisualisePSType
{
Cube = 0,
Texture1D = 1, //not supported
Texture2DNoMSAA = 2,
Texture3D = 3,
CubeArray = 4,
Texture2DMSAA = 5,
Texture2DDepthStencilNoMSAA = 6,
Texture2DUINT8 = 7,
Texture2DUINT32 = 8,
MAX
};
/** A pixel shader which filters a texture. */
// @param TextureType 0:Cube, 1:1D(not yet supported), 2:2D no MSAA, 3:3D, 4:Cube[], 5:2D MSAA, 6:2D DepthStencil no MSAA (needed to avoid D3DDebug error)
class FVisualizeTexturePS : public FGlobalShader
{
DECLARE_GLOBAL_SHADER(FVisualizeTexturePS);
SHADER_USE_PARAMETER_STRUCT(FVisualizeTexturePS, FGlobalShader);
class FVisualisePSTypeDim : SHADER_PERMUTATION_ENUM_CLASS("TEXTURE_TYPE", EVisualisePSType);
using FPermutationDomain = TShaderPermutationDomain<FVisualisePSTypeDim>;
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
FPermutationDomain PermutationVector(Parameters.PermutationId);
return PermutationVector.Get<FVisualisePSTypeDim>() != EVisualisePSType::Texture1D;
}
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
SHADER_PARAMETER(FVector3f, TextureExtent)
SHADER_PARAMETER_ARRAY(FVector4f, VisualizeParam, [3])
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, VisualizeTexture2D)
SHADER_PARAMETER_SAMPLER(SamplerState, VisualizeTexture2DSampler)
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture3D, VisualizeTexture3D)
SHADER_PARAMETER_SAMPLER(SamplerState, VisualizeTexture3DSampler)
SHADER_PARAMETER_RDG_TEXTURE_SRV(TextureCube, VisualizeTextureCube)
SHADER_PARAMETER_SAMPLER(SamplerState, VisualizeTextureCubeSampler)
SHADER_PARAMETER_RDG_TEXTURE_SRV(TextureCubeArray, VisualizeTextureCubeArray)
SHADER_PARAMETER_SAMPLER(SamplerState, VisualizeTextureCubeArraySampler)
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D<uint4>, VisualizeDepthStencil)
SHADER_PARAMETER_RDG_TEXTURE(Texture2DMS<float4>, VisualizeTexture2DMS)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D<uint>, VisualizeUINT8Texture2D)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
};
IMPLEMENT_GLOBAL_SHADER(FVisualizeTexturePS, "/Engine/Private/Tools/VisualizeTexture.usf", "VisualizeTexturePS", SF_Pixel);
static EVisualisePSType GetVisualizePSType(const FRDGTextureDesc& Desc)
{
if(Desc.IsTexture2D())
{
// 2D
if(Desc.NumSamples > 1)
{
// MSAA
return EVisualisePSType::Texture2DMSAA;
}
else
{
if(Desc.Format == PF_DepthStencil)
{
// DepthStencil non MSAA (needed to avoid D3DDebug error)
return EVisualisePSType::Texture2DDepthStencilNoMSAA;
}
else if (Desc.Format == PF_R8_UINT)
{
return EVisualisePSType::Texture2DUINT8;
}
else if (Desc.Format == PF_R32_UINT)
{
return EVisualisePSType::Texture2DUINT32;
}
else
{
// non MSAA
return EVisualisePSType::Texture2DNoMSAA;
}
}
}
else if(Desc.IsTextureCube())
{
if(Desc.IsTextureArray())
{
// Cube[]
return EVisualisePSType::CubeArray;
}
else
{
// Cube
return EVisualisePSType::Cube;
}
}
check(Desc.IsTexture3D());
return EVisualisePSType::Texture3D;
}
void FVisualizeTexture::ReleaseRHI()
{
Config = {};
Requested = {};
Captured = {};
}
// static
FRDGTextureRef FVisualizeTexture::AddVisualizeTexturePass(
FRDGBuilder& GraphBuilder,
FGlobalShaderMap* ShaderMap,
const FRDGTextureRef InputTexture,
const FConfig& VisualizeConfig,
EInputValueMapping InputValueMapping,
uint32 CaptureId)
{
check(InputTexture);
check(!EnumHasAnyFlags(InputTexture->Desc.Flags, TexCreate_CPUReadback));
const FRDGTextureDesc& InputDesc = InputTexture->Desc;
FIntPoint InputExtent = InputDesc.Extent;
FIntPoint OutputExtent = InputExtent;
// Scene textures are padded and shared across scene renderers, with a given scene renderer using a viewport in the shared buffer.
// We only want to visualize the portion actually used by the given scene renderer, as the rest will be blank or garbage. The info
// text will display the actual texture size in addition to the viewport being visualized.
FIntPoint VisualizeTextureExtent = InputTexture->GetVisualizeExtent();
if ((VisualizeTextureExtent.X > 0) && (VisualizeTextureExtent.Y > 0))
{
// Clamp extent at actual dimensions of texture
OutputExtent.X = FMath::Min(VisualizeTextureExtent.X, OutputExtent.X);
OutputExtent.Y = FMath::Min(VisualizeTextureExtent.Y, OutputExtent.Y);
}
if (InputDesc.IsTextureCube())
{
// For pixel perfect display of cube map, we'll use a 4x3 flat unwrapping of the cube map, rather than a projection. The visualization
// shader detects the 4x3 aspect ratio, and generates a seamless panorama in the middle, with the adjacent floor and sky tiles above
// and below. There will be seams between floor and sky tiles, but the pixels shown will otherwise be exact.
if (GVisualizeTexture.Config.InputUVMapping == EInputUVMapping::PixelPerfectCenter)
{
InputExtent.X *= 4;
InputExtent.Y *= 3;
OutputExtent.X *= 4;
OutputExtent.Y *= 3;
}
else
{
// Longitudinal rendered cube maps look better with 2 to 1 aspect ratio (same as how the texture resource viewer displays cube maps)
InputExtent.X *= 2;
OutputExtent.X *= 2;
}
}
GVisualizeTexture.Captured.OutputExtent = OutputExtent;
// Clamp to reasonable value to prevent crash
OutputExtent.X = FMath::Max(OutputExtent.X, 1);
OutputExtent.Y = FMath::Max(OutputExtent.Y, 1);
FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D(OutputExtent, PF_B8G8R8A8, FClearValueBinding(FLinearColor(1, 1, 0, 1)), TexCreate_RenderTargetable | TexCreate_ShaderResource), InputTexture->Name);
{
const EVisualisePSType VisualizeType = GetVisualizePSType(InputDesc);
FVisualizeTexturePS::FParameters* PassParameters = GraphBuilder.AllocParameters<FVisualizeTexturePS::FParameters>();
{
PassParameters->TextureExtent = FVector3f(InputExtent.X, InputExtent.Y, InputDesc.Depth);
{
// Alternates between 0 and 1 with a short pause
const float FracTimeScale = 1.0f / 4.0f;
float FracTime = FApp::GetCurrentTime() * FracTimeScale - floor(FApp::GetCurrentTime() * FracTimeScale);
float BlinkState = (FracTime < 1.0f / 16.0f) ? 1.0f : 0.0f;
FVector4f VisualizeParamValue[3];
float Add = 0.0f;
float FracScale = 1.0f;
// w * almost_1 to avoid frac(1) => 0
PassParameters->VisualizeParam[0] = FVector4f(VisualizeConfig.RGBMul, VisualizeConfig.SingleChannelMul, Add, FracScale * 0.9999f);
PassParameters->VisualizeParam[1] = FVector4f(CVarAllowBlinking.GetValueOnRenderThread() ? BlinkState : 0.0f, (VisualizeConfig.ShaderOp == EShaderOp::Saturate) ? 1.0f : 0.0f, VisualizeConfig.ArrayIndex, VisualizeConfig.MipIndex);
PassParameters->VisualizeParam[2] = FVector4f((float)InputValueMapping, 0.0f, VisualizeConfig.SingleChannel);
}
FRDGTextureSRV* InputSRV = nullptr;
FRHISamplerState* PointSampler = TStaticSamplerState<SF_Point, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
if (InputTexture->Desc.Dimension == ETextureDimension::Texture2DArray)
{
InputSRV = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForSlice(InputTexture, FMath::Clamp(int32(VisualizeConfig.ArrayIndex), 0, int32(InputDesc.ArraySize) - 1)));
}
else
{
InputSRV = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::Create(InputTexture));
}
PassParameters->VisualizeTexture2D = InputSRV;
PassParameters->VisualizeTexture2DSampler = PointSampler;
PassParameters->VisualizeTexture3D = InputSRV;
PassParameters->VisualizeTexture3DSampler = PointSampler;
PassParameters->VisualizeTextureCube = InputSRV;
PassParameters->VisualizeTextureCubeSampler = PointSampler;
PassParameters->VisualizeTextureCubeArray = InputSRV;
PassParameters->VisualizeTextureCubeArraySampler = PointSampler;
if (VisualizeType == EVisualisePSType::Texture2DDepthStencilNoMSAA)
{
FRDGTextureSRVDesc SRVDesc = FRDGTextureSRVDesc::CreateWithPixelFormat(InputTexture, PF_X24_G8);
PassParameters->VisualizeDepthStencil = GraphBuilder.CreateSRV(SRVDesc);
}
PassParameters->VisualizeTexture2DMS = InputTexture;
PassParameters->VisualizeUINT8Texture2D = InputTexture;
PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::EClear);
}
FVisualizeTexturePS::FPermutationDomain PermutationVector;
PermutationVector.Set<FVisualizeTexturePS::FVisualisePSTypeDim>(VisualizeType);
TShaderMapRef<FVisualizeTexturePS> PixelShader(ShaderMap, PermutationVector);
FString ExtendedDrawEvent;
if (GraphBuilder.ShouldEmitEvents())
{
if (InputDesc.IsTexture3D())
{
ExtendedDrawEvent += FString::Printf(TEXT("x%d CapturedSlice=%d"), InputDesc.Depth, VisualizeConfig.ArrayIndex);
}
if (InputDesc.IsTextureArray())
{
ExtendedDrawEvent += FString::Printf(TEXT(" ArraySize=%d CapturedSlice=%d"), InputDesc.ArraySize, VisualizeConfig.ArrayIndex);
}
// Precise the mip level being captured in the mip level when there is a mip chain.
if (InputDesc.IsMipChain())
{
ExtendedDrawEvent += FString::Printf(TEXT(" Mips=%d CapturedMip=%d"), InputDesc.NumMips, VisualizeConfig.MipIndex);
}
}
FPixelShaderUtils::AddFullscreenPass(
GraphBuilder,
ShaderMap,
RDG_EVENT_NAME("VisualizeTextureCapture(%s@%d %s %dx%d%s)",
InputTexture->Name, CaptureId,
GPixelFormats[InputDesc.Format].Name,
InputExtent.X, InputExtent.Y,
*ExtendedDrawEvent),
PixelShader,
PassParameters,
FIntRect(0, 0, OutputExtent.X, OutputExtent.Y));
}
return OutputTexture;
}
void FVisualizeTexture::CreateContentCapturePass(FRDGBuilder& GraphBuilder, const FRDGTextureRef InputTexture, uint32 CaptureId)
{
if (!InputTexture)
{
return;
}
const FRDGTextureDesc& InputDesc = InputTexture->Desc;
const FIntPoint InputExtent = InputDesc.Extent;
if (EnumHasAnyFlags(InputDesc.Flags, TexCreate_CPUReadback))
{
return;
}
EInputValueMapping InputValueMapping = EInputValueMapping::Color;
{
if (InputDesc.Format == PF_ShadowDepth)
{
InputValueMapping = EInputValueMapping::Shadow;
}
else if (EnumHasAnyFlags(InputDesc.Flags, TexCreate_DepthStencilTargetable))
{
InputValueMapping = EInputValueMapping::Depth;
}
}
FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(FeatureLevel);
FRDGTextureRef OutputTexture = AddVisualizeTexturePass(GraphBuilder, ShaderMap, InputTexture, Config, InputValueMapping, CaptureId);
FIntPoint OutputExtent = InputExtent;
OutputExtent.X = FMath::Max(OutputExtent.X, 1);
OutputExtent.Y = FMath::Max(OutputExtent.Y, 1);
{
Captured.Desc = Translate(InputDesc);
Captured.Desc.DebugName = InputTexture->Name;
Captured.PooledRenderTarget = nullptr;
Captured.Texture = OutputTexture;
Captured.InputValueMapping = InputValueMapping;
Captured.ViewRects = FamilyViewRects;
GraphBuilder.QueueTextureExtraction(OutputTexture, &Captured.PooledRenderTarget);
}
if (EnumHasAnyFlags(Config.Flags, EFlags::SaveBitmap | EFlags::SaveBitmapAsStencil))
{
uint32 MipAdjustedExtentX = FMath::Clamp(OutputExtent.X >> Config.MipIndex, 0, OutputExtent.X);
uint32 MipAdjustedExtentY = FMath::Clamp(OutputExtent.Y >> Config.MipIndex, 0, OutputExtent.Y);
FIntPoint Extent(MipAdjustedExtentX, MipAdjustedExtentY);
FReadSurfaceDataFlags ReadDataFlags;
ReadDataFlags.SetLinearToGamma(false);
ReadDataFlags.SetOutputStencil(EnumHasAnyFlags(Config.Flags, EFlags::SaveBitmapAsStencil));
ReadDataFlags.SetMip(Config.MipIndex);
const TCHAR* DebugName = Captured.Desc.DebugName;
AddReadbackTexturePass(GraphBuilder, RDG_EVENT_NAME("SaveBitmap"), OutputTexture,
[OutputTexture, Extent, ReadDataFlags, DebugName](FRHICommandListImmediate& RHICmdList)
{
TArray<FColor> Bitmap;
RHICmdList.ReadSurfaceData(OutputTexture->GetRHI(), FIntRect(0, 0, Extent.X, Extent.Y), Bitmap, ReadDataFlags);
// if the format and texture type is supported
if (Bitmap.Num())
{
// Create screenshot folder if not already present.
IFileManager::Get().MakeDirectory(*FPaths::ScreenShotDir(), true);
const FString Filename(FPaths::ScreenShotDir() / TEXT("VisualizeTexture"));
uint32 ExtendXWithMSAA = Bitmap.Num() / Extent.Y;
// Save the contents of the array to a bitmap file. (24bit only so alpha channel is dropped)
FFileHelper::CreateBitmap(*Filename, ExtendXWithMSAA, Extent.Y, Bitmap.GetData());
UE_LOG(LogRendererCore, Display, TEXT("Content was saved to \"%s\""), *FPaths::ScreenShotDir());
}
else
{
UE_LOG(LogRendererCore, Error, TEXT("Failed to save BMP for VisualizeTexture, format or texture type is not supported"));
}
});
}
}
void FVisualizeTexture::BeginFrameRenderThread()
{
bAnyViewRendered = false;
bIsRequestedView = false;
bFoundRequestedView = false;
}
static bool VisualizeTextureViewNameMatches(const FString& ViewName, const TCHAR* Description)
{
// Description will be of the form "EditorName (FName)" or "Name", with EditorName being user facing. Match name followed by space or null terminator.
int32 ViewNameLen = ViewName.Len();
return !FCString::Strnicmp(*ViewName, Description, ViewNameLen) && (Description[ViewNameLen] == ' ' || Description[ViewNameLen] == 0);
}
void FVisualizeTexture::BeginViewRenderThread(ERHIFeatureLevel::Type InFeatureLevel, int32 UniqueId, const TCHAR* Description, bool bIsSceneCapture)
{
// Only support visualization for views with a unique ID
if (State == EState::Inactive || !UniqueId)
{
return;
}
FeatureLevel = InFeatureLevel;
if (!bAnyViewRendered)
{
// Clear list of views out when we encounter the first view on the current frame
ViewDescriptionMap.Empty();
bAnyViewRendered = true;
}
ViewDescriptionMap.FindOrAdd(UniqueId) = Description;
if (!Requested.ViewName.IsEmpty() ? VisualizeTextureViewNameMatches(Requested.ViewName, Description) : Requested.ViewUniqueId == UniqueId)
{
// Found the specific view we requested
bIsRequestedView = true;
bFoundRequestedView = true;
}
else if (!bFoundRequestedView)
{
// If specific requested view hasn't been found, visualize any view that's not a scene capture, so we still get some sort of result
bIsRequestedView = !bIsSceneCapture;
}
// Clear outputs when we are processing a requested view
if (bIsRequestedView)
{
VersionCountMap.Empty();
Captured = {};
Captured.ViewUniqueId = UniqueId;
}
}
void FVisualizeTexture::SetSceneTextures(const TArray<FRDGTextureRef>& InSceneTextures, FIntPoint InFamilySize, const TArray<FIntRect>& InFamilyViewRects)
{
for (FRDGTextureRef Texture : InSceneTextures)
{
Texture->EncloseVisualizeExtent(InFamilySize);
}
FamilyViewRects = InFamilyViewRects;
}
TOptional<uint32> FVisualizeTexture::ShouldCapture(const TCHAR* InName, uint32 InMipIndex)
{
TOptional<uint32> CaptureId;
uint32& VersionCount = VersionCountMap.FindOrAdd(InName);
if (!Requested.Name.IsEmpty() && Requested.Name == InName)
{
if (!Requested.Version || VersionCount == Requested.Version.GetValue())
{
CaptureId = VersionCount;
}
}
++VersionCount;
return CaptureId;
}
void FVisualizeTexture::EndViewRenderThread()
{
if (bIsRequestedView)
{
bIsRequestedView = false;
FamilyViewRects.Empty();
}
}
void FVisualizeTexture::EndFrameRenderThread()
{
if (bAnyViewRendered)
{
if (State == EState::DisplayResources)
{
DisplayResourceListToLog(DisplayResourcesParam);
DisplayResourcesParam.Reset();
State = EState::TrackResources;
}
else if (State == EState::DisplayViews)
{
DisplayViewListToLog();
State = EState::TrackResources;
}
}
}
uint32 FVisualizeTexture::GetVersionCount(const TCHAR* InName) const
{
if (const uint32* VersionCount = VersionCountMap.Find(InName))
{
return *VersionCount;
}
return 0;
}
void FVisualizeTexture::SetCheckPoint(FRDGBuilder& GraphBuilder, IPooledRenderTarget* PooledRenderTarget)
{
check(IsInRenderingThread());
if (!PooledRenderTarget)
{
return;
}
const FPooledRenderTargetDesc& Desc = PooledRenderTarget->GetDesc();
if (!EnumHasAnyFlags(Desc.Flags, TexCreate_ShaderResource))
{
return;
}
TOptional<uint32> CaptureId = ShouldCapture(Desc.DebugName, Config.MipIndex);
if (!CaptureId)
{
return;
}
FRDGTextureRef TextureToCapture = GraphBuilder.RegisterExternalTexture(PooledRenderTarget);
CreateContentCapturePass(GraphBuilder, TextureToCapture, CaptureId.GetValue());
}
void FVisualizeTexture::SetCheckPoint(FRHICommandListImmediate& RHICmdList, IPooledRenderTarget* PooledRenderTarget)
{
FRDGBuilder GraphBuilder(RHICmdList);
SetCheckPoint(GraphBuilder, PooledRenderTarget);
GraphBuilder.Execute();
}
void FVisualizeTexture::Visualize(const FString& InName, TOptional<uint32> InVersion)
{
Requested.Name = InName;
Requested.Version = InVersion;
}
#endif // SUPPORTS_VISUALIZE_TEXTURE
// static
FRDGTextureRef FVisualizeTexture::AddVisualizeTexturePass(
FRDGBuilder& GraphBuilder,
class FGlobalShaderMap* ShaderMap,
const FRDGTextureRef InputTexture)
#if SUPPORTS_VISUALIZE_TEXTURE
{
check(InputTexture);
EInputValueMapping InputValueMapping = EInputValueMapping::Color;
{
if (InputTexture->Desc.Format == PF_ShadowDepth)
{
InputValueMapping = EInputValueMapping::Shadow;
}
else if (EnumHasAnyFlags(InputTexture->Desc.Flags, TexCreate_DepthStencilTargetable))
{
InputValueMapping = EInputValueMapping::Depth;
}
}
FConfig VisualizeConfig;
return AddVisualizeTexturePass(GraphBuilder, ShaderMap, InputTexture, VisualizeConfig, InputValueMapping, /* CaptureId = */ 0);
}
#else
{
return InputTexture;
}
#endif
// static
FRDGTextureRef FVisualizeTexture::AddVisualizeTextureAlphaPass(
FRDGBuilder& GraphBuilder,
class FGlobalShaderMap* ShaderMap,
const FRDGTextureRef InputTexture)
#if SUPPORTS_VISUALIZE_TEXTURE
{
check(InputTexture);
FConfig VisualizeConfig;
VisualizeConfig.SingleChannel = 3;
VisualizeConfig.SingleChannelMul = 1.0f;
VisualizeConfig.RGBMul = 0.0f;
return AddVisualizeTexturePass(GraphBuilder, ShaderMap, InputTexture, VisualizeConfig, EInputValueMapping::Color, /* CaptureId = */ 0);
}
#else
{
return InputTexture;
}
#endif