Files
UnrealEngine/Engine/Plugins/Mutable/Source/MutableTools/Private/MuT/ASTOpSwitch.cpp
2025-05-18 13:04:45 +08:00

774 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MuT/ASTOpSwitch.h"
#include "MuT/ASTOpParameter.h"
#include "MuT/ASTOpConstantInt.h"
#include "MuT/ASTOpMeshAddMetadata.h"
#include "Containers/Map.h"
#include "Misc/AssertionMacros.h"
#include "MuR/ImagePrivate.h"
#include "MuR/ModelPrivate.h"
#include "MuR/MutableMath.h"
#include "MuR/RefCounted.h"
#include "MuR/Types.h"
namespace mu
{
ASTOpSwitch::ASTOpSwitch()
: Variable(this)
, Default(this)
{
}
ASTOpSwitch::~ASTOpSwitch()
{
// Explicit call needed to avoid recursive destruction
ASTOp::RemoveChildren();
}
bool ASTOpSwitch::IsEqual(const ASTOp& otherUntyped) const
{
if (GetOpType()==otherUntyped.GetOpType())
{
const ASTOpSwitch* other = static_cast<const ASTOpSwitch*>(&otherUntyped);
return Type == other->Type && Variable == other->Variable &&
Cases == other->Cases && Default == other->Default;
}
return false;
}
mu::Ptr<ASTOp> ASTOpSwitch::Clone(MapChildFuncRef mapChild) const
{
Ptr<ASTOpSwitch> n = new ASTOpSwitch();
n->Type = Type;
n->Variable = mapChild(Variable.child());
n->Default = mapChild(Default.child());
for (const FCase& c : Cases)
{
n->Cases.Emplace(c.Condition, n, mapChild(c.Branch.child()));
}
return n;
}
void ASTOpSwitch::Assert()
{
switch (Type)
{
case EOpType::NU_SWITCH:
case EOpType::SC_SWITCH:
case EOpType::CO_SWITCH:
case EOpType::IM_SWITCH:
case EOpType::ME_SWITCH:
case EOpType::LA_SWITCH:
case EOpType::IN_SWITCH:
case EOpType::ED_SWITCH:
break;
default:
// Unexpected Type
check(false);
break;
}
ASTOp::Assert();
}
uint64 ASTOpSwitch::Hash() const
{
uint64 res = std::hash<uint64>()(uint64(Type));
for (const FCase& c : Cases)
{
hash_combine(res, c.Condition);
hash_combine(res, c.Branch.child().get());
}
return res;
}
mu::Ptr<ASTOp> ASTOpSwitch::GetFirstValidValue()
{
for (int32 i = 0; i < Cases.Num(); ++i)
{
if (Cases[i].Branch)
{
return Cases[i].Branch.child();
}
}
return nullptr;
}
bool ASTOpSwitch::IsCompatibleWith(const ASTOpSwitch* other) const
{
if (!other) return false;
if (Variable.child() != other->Variable.child()) return false;
if (Cases.Num() != other->Cases.Num()) return false;
for (const FCase& c : Cases)
{
bool found = false;
for (const FCase& o : other->Cases)
{
if (c.Condition == o.Condition)
{
found = true;
break;
}
}
if (!found)
{
return false;
}
}
return true;
}
mu::Ptr<ASTOp> ASTOpSwitch::FindBranch(int32 Condition) const
{
for (const FCase& c : Cases)
{
if (c.Condition == Condition)
{
return c.Branch.child();
}
}
return Default.child();
}
void ASTOpSwitch::ForEachChild(const TFunctionRef<void(ASTChild&)> f)
{
f(Variable);
f(Default);
for (FCase& cas : Cases)
{
f(cas.Branch);
}
}
void ASTOpSwitch::Link(FProgram& program, FLinkerOptions*)
{
// Already linked?
if (!linkedAddress)
{
linkedAddress = (OP::ADDRESS)program.OpAddress.Num();
program.OpAddress.Add((uint32_t)program.ByteCode.Num());
OP::ADDRESS VarAddress = Variable ? Variable->linkedAddress : 0;
OP::ADDRESS DefAddress = Default ? Default->linkedAddress : 0;
AppendCode(program.ByteCode, Type);
AppendCode(program.ByteCode, VarAddress);
AppendCode(program.ByteCode, DefAddress);
AppendCode(program.ByteCode, (uint32_t)Cases.Num());
for (const FCase& Case : Cases)
{
OP::ADDRESS CaseBranchAddress = Case.Branch ? Case.Branch->linkedAddress : 0;
AppendCode(program.ByteCode, Case.Condition);
AppendCode(program.ByteCode, CaseBranchAddress);
}
}
}
FImageDesc ASTOpSwitch::GetImageDesc(bool bReturnBestOption, class FGetImageDescContext* Context) const
{
FImageDesc Result;
// Local context in case it is necessary
FGetImageDescContext LocalContext;
if (!Context)
{
Context = &LocalContext;
}
else
{
// Cached result?
FImageDesc* PtrValue = Context->m_results.Find(this);
if (PtrValue)
{
return *PtrValue;
}
}
// In a switch we cannot guarantee the size and format.
// We check all the options, and if they are the same we return that.
// Otherwise, we return a descriptor with empty fields in the conflicting ones, size or format.
// In some places this will force re-formatting of the image.
// The code optimiser will take care then of moving the format operations down to each
// Branch and remove the unnecessary ones.
FImageDesc Candidate;
bool bSameSize = true;
bool bSameFormat = true;
bool bSameLods = true;
bool bFirst = true;
if (Default)
{
FImageDesc ChildDesc = Default->GetImageDesc(bReturnBestOption, Context);
Candidate = ChildDesc;
bFirst = false;
}
for (int32 CaseIndex = 0; CaseIndex < Cases.Num(); ++CaseIndex)
{
if (Cases[CaseIndex].Branch)
{
FImageDesc ChildDesc = Cases[CaseIndex].Branch->GetImageDesc(bReturnBestOption, Context);
if (bFirst)
{
Candidate = ChildDesc;
bFirst = false;
}
else
{
bSameSize = bSameSize && (Candidate.m_size == ChildDesc.m_size);
bSameFormat = bSameFormat && (Candidate.m_format == ChildDesc.m_format);
bSameLods = bSameLods && (Candidate.m_lods == ChildDesc.m_lods);
if (bReturnBestOption)
{
Candidate.m_format = GetMostGenericFormat(Candidate.m_format, ChildDesc.m_format);
// Return the biggest size
Candidate.m_size[0] = FMath::Max(Candidate.m_size[0], ChildDesc.m_size[0]);
Candidate.m_size[1] = FMath::Max(Candidate.m_size[1], ChildDesc.m_size[1]);
}
}
}
}
Result = Candidate;
// In case of ReturnBestOption the first valid case will be used to determine size and lods.
// Format will be the most generic from all Cases.
if (!bSameFormat && !bReturnBestOption)
{
Result.m_format = EImageFormat::None;
}
if (!bSameSize && !bReturnBestOption)
{
Result.m_size = FImageSize(0, 0);
}
if (!bSameLods && !bReturnBestOption)
{
Result.m_lods = 0;
}
// Cache the result
if (Context)
{
Context->m_results.Add(this, Result);
}
return Result;
}
void ASTOpSwitch::GetBlockLayoutSize(uint64 BlockId, int32* pBlockX, int32* pBlockY, FBlockLayoutSizeCache* cache)
{
switch (Type)
{
case EOpType::LA_SWITCH:
{
Ptr<ASTOp> child = GetFirstValidValue();
if (!child)
{
child = Default.child();
}
if (child)
{
child->GetBlockLayoutSizeCached(BlockId, pBlockX, pBlockY, cache);
}
else
{
*pBlockX = 0;
*pBlockY = 0;
}
break;
}
default:
check(false);
break;
}
}
void ASTOpSwitch::GetLayoutBlockSize(int32* pBlockX, int32* pBlockY)
{
switch (Type)
{
case EOpType::IM_SWITCH:
{
Ptr<ASTOp> child = GetFirstValidValue();
if (!child)
{
child = Default.child();
}
if (child)
{
child->GetLayoutBlockSize(pBlockX, pBlockY);
}
else
{
checkf(false, TEXT("Image switch had no options."));
}
break;
}
default:
checkf(false, TEXT("Instruction not supported"));
}
}
bool ASTOpSwitch::GetNonBlackRect(FImageRect& maskUsage) const
{
if (Type == EOpType::IM_SWITCH)
{
FImageRect local;
bool localValid = false;
if (Default)
{
localValid = Default->GetNonBlackRect(local);
if (!localValid)
{
return false;
}
}
for (const FCase& c : Cases)
{
if (c.Branch)
{
FImageRect branchRect;
bool validBranch = c.Branch->GetNonBlackRect(branchRect);
if (validBranch)
{
if (localValid)
{
local.Bound(branchRect);
}
else
{
local = branchRect;
localValid = true;
}
}
else
{
return false;
}
}
}
if (localValid)
{
maskUsage = local;
return true;
}
}
return false;
}
bool ASTOpSwitch::IsImagePlainConstant(FVector4f&) const
{
// We could check if every option is plain and exactly the same colour, but probably it is
// not worth.
return false;
}
mu::Ptr<ASTOp> ASTOpSwitch::OptimiseSemantic(const FModelOptimizationOptions&, int32 Pass) const
{
// Constant Condition?
if (Variable->GetOpType() == EOpType::NU_CONSTANT)
{
Ptr<ASTOp> Branch = Default.child();
const ASTOpConstantInt* typedCondition = static_cast<const ASTOpConstantInt*>(Variable.child().get());
for (int32 o = 0; o < Cases.Num(); ++o)
{
if (Cases[o].Branch &&
typedCondition->Value == (int32)Cases[o].Condition)
{
Branch = Cases[o].Branch.child();
break;
}
}
return Branch;
}
else if (Variable->GetOpType() == EOpType::NU_PARAMETER)
{
// If all the branches for the possible values are the same op remove the instruction
const ASTOpParameter* ParamOp = static_cast<const ASTOpParameter*>(Variable.child().get());
check(ParamOp);
if(ParamOp->Parameter.PossibleValues.IsEmpty())
{
return nullptr;
}
bool bFirstValue = true;
bool bAllSame = true;
Ptr<ASTOp> SameBranch = nullptr;
for (const FParameterDesc::FIntValueDesc& Value : ParamOp->Parameter.PossibleValues)
{
// Look for the switch Branch it would take
Ptr<ASTOp> Branch = Default.child();
for (const FCase& Case : Cases)
{
if (Case.Condition == Value.Value)
{
Branch = Case.Branch.child();
break;
}
}
if (bFirstValue)
{
bFirstValue = false;
SameBranch = Branch;
}
else
{
if (SameBranch != Branch)
{
bAllSame = false;
SameBranch = nullptr;
break;
}
}
}
if (bAllSame)
{
return SameBranch;
}
}
// Ad-hoc logic optimization: check if all code paths leading to this operation have a switch with the same Variable
// and the option on those switches for the path that connects to this one is always the same. In that case, we can
// remove this switch and replace it by the value it has for that option.
// This is something the generic logic optimizer should do whan re-enabled.
{
// List of parent operations that we have visited, and the child we have visited them from.
TSet<TTuple<const ASTOp*, const ASTOp*>> Visited;
Visited.Reserve(64);
// First is parent, second is what child we are reaching the parent from. This is necessary to find out what
// switch Branch we reach the parent from, if it is a switch.
TArray< TTuple<const ASTOp*, const ASTOp*>, TInlineAllocator<16>> Pending;
ForEachParent([this,&Pending](ASTOp* Parent)
{
Pending.Add({ Parent,this});
});
bool bAllPathsHaveMatchingSwitch = true;
// Switch option value of all parent compatible switches (if any)
int32 MatchingSwitchOption = -1;
while (!Pending.IsEmpty() && bAllPathsHaveMatchingSwitch)
{
TTuple<const ASTOp*, const ASTOp*> ParentPair = Pending.Pop();
bool bAlreadyVisited = false;
Visited.Add(ParentPair, &bAlreadyVisited);
if (!bAlreadyVisited)
{
const ASTOp* Parent = ParentPair.Get<0>();
const ASTOp* ParentChild = ParentPair.Get<1>();
bool bIsMatchingSwitch = false;
// TODO: Probably it could be a any switch, it doesn't need to be of the same Type.
if (Parent->GetOpType() == GetOpType())
{
const ASTOpSwitch* ParentSwitch = static_cast<const ASTOpSwitch*>(Parent);
check(ParentSwitch);
// To be compatible the switch must be on the same Variable
if (ParentSwitch->Variable==Variable)
{
bIsMatchingSwitch = true;
// Find what switch option we are reaching it from
bool bIsSingleOption = true;
int OptionIndex = -1;
for (int32 CaseIndex = 0; CaseIndex < ParentSwitch->Cases.Num(); ++CaseIndex)
{
if (ParentSwitch->Cases[CaseIndex].Branch.child().get() == ParentChild)
{
if (OptionIndex != -1)
{
// This means the same child is connected to more than one switch options
// so we cannot optimize.
// \TODO: We could if we track a "set of options" for all switches instead of just one.
bIsSingleOption = false;
break;
}
else
{
OptionIndex = CaseIndex;
}
}
}
// If we did reach it from one single option
if (bIsSingleOption && OptionIndex!=-1)
{
if (MatchingSwitchOption<0)
{
MatchingSwitchOption = ParentSwitch->Cases[OptionIndex].Condition;
}
else if (MatchingSwitchOption!= ParentSwitch->Cases[OptionIndex].Condition)
{
bAllPathsHaveMatchingSwitch = false;
}
}
}
}
if (!bIsMatchingSwitch)
{
// If it has no parents, then the optimization cannot be applied
bool bHasParent = false;
Parent->ForEachParent([&bHasParent,this,&Pending,Parent](ASTOp* ParentParent)
{
Pending.Add({ ParentParent,Parent });
bHasParent = true;
});
if (!bHasParent)
{
// We reached a root without a matching switch along the path.
bAllPathsHaveMatchingSwitch = false;
}
}
}
}
if (bAllPathsHaveMatchingSwitch && MatchingSwitchOption>=0)
{
// We can remove this switch, all paths leading to it have the same Condition for this switches Variable.
return FindBranch(MatchingSwitchOption);
}
}
return nullptr;
}
Ptr<ASTOp> ASTOpSwitch::OptimiseSink(const FModelOptimizationOptions&, FOptimizeSinkContext&) const
{
Ptr<ASTOp> NewOp;
// Detect if all Cases are the same op Type or they are null (same op with some branches being null).
EOpType BranchOpType = EOpType::NONE;
bool bSameOpTypeOrNull = true;
if (Default)
{
BranchOpType = Default->GetOpType();
}
for (const FCase& Case : Cases)
{
if (!Case.Branch)
{
continue;
}
if (BranchOpType==EOpType::NONE)
{
BranchOpType = Case.Branch->GetOpType();
}
else if (Case.Branch->GetOpType() != BranchOpType)
{
bSameOpTypeOrNull = false;
break;
}
}
if (bSameOpTypeOrNull)
{
switch (BranchOpType)
{
case EOpType::ME_ADDMETADATA:
{
// Move the add tags out of the switch if all tags are the same
bool bAllMetadataIsTheSame = true;
TArray<FString> Tags;
TArray<uint64> ResourceIds;
TArray<uint32> SkeletonIds;
if (Default)
{
check(Default->GetOpType() == EOpType::ME_ADDMETADATA);
const ASTOpMeshAddMetadata* Typed = static_cast<const ASTOpMeshAddMetadata*>(Default.child().get());
Tags = Typed->Tags;
ResourceIds = Typed->ResourceIds;
SkeletonIds = Typed->SkeletonIds;
}
for (const FCase& Case : Cases)
{
if (!Case.Branch)
{
continue;
}
check(Case.Branch->GetOpType() == EOpType::ME_ADDMETADATA);
const ASTOpMeshAddMetadata* Typed = static_cast<const ASTOpMeshAddMetadata*>(Case.Branch.child().get());
if (Tags.IsEmpty() && ResourceIds.IsEmpty() && SkeletonIds.IsEmpty())
{
Tags = Typed->Tags;
ResourceIds = Typed->ResourceIds;
SkeletonIds = Typed->SkeletonIds;
}
else if (
Typed->Tags != Tags ||
Typed->ResourceIds != ResourceIds ||
Typed->SkeletonIds != SkeletonIds)
{
bAllMetadataIsTheSame = false;
break;
}
}
if (bAllMetadataIsTheSame)
{
Ptr<ASTOpMeshAddMetadata> New = new ASTOpMeshAddMetadata();
New->Tags = Tags;
New->ResourceIds = ResourceIds;
New->SkeletonIds = SkeletonIds;
{
Ptr<ASTOpSwitch> NewSwitch = mu::Clone<ASTOpSwitch>(this);
// Replace all branches removing the "add tags" operation.
if (Default)
{
check(Default->GetOpType() == EOpType::ME_ADDMETADATA);
const ASTOpMeshAddMetadata* Typed = static_cast<const ASTOpMeshAddMetadata*>(Default.child().get());
NewSwitch->Default = Typed->Source.child();
}
for (int32 CaseIndex = 0; CaseIndex < Cases.Num(); ++CaseIndex)
{
const FCase& SourceCase = Cases[CaseIndex];
if (!SourceCase.Branch)
{
continue;
}
FCase& NewCase = NewSwitch->Cases[CaseIndex];
check(SourceCase.Branch->GetOpType() == EOpType::ME_ADDMETADATA);
const ASTOpMeshAddMetadata* Typed = static_cast<const ASTOpMeshAddMetadata*>(SourceCase.Branch.child().get());
NewCase.Branch = Typed->Source.child();
}
New->Source = NewSwitch;
}
NewOp = New;
}
break;
}
default:
break;
}
}
return NewOp;
}
mu::Ptr<ImageSizeExpression> ASTOpSwitch::GetImageSizeExpression() const
{
Ptr<ImageSizeExpression> pRes = new ImageSizeExpression;
bool first = true;
for (const FCase& c : Cases)
{
if (c.Branch)
{
if (first)
{
pRes = c.Branch->GetImageSizeExpression();
}
else
{
Ptr<ImageSizeExpression> pOther = c.Branch->GetImageSizeExpression();
if (!(*pOther == *pRes))
{
pRes->type = ImageSizeExpression::ISET_UNKNOWN;
break;
}
}
}
}
return pRes;
}
FSourceDataDescriptor ASTOpSwitch::GetSourceDataDescriptor(FGetSourceDataDescriptorContext* Context) const
{
// Cache management
TUniquePtr<FGetSourceDataDescriptorContext> LocalContext;
if (!Context)
{
LocalContext.Reset(new FGetSourceDataDescriptorContext);
Context = LocalContext.Get();
}
FSourceDataDescriptor* Found = Context->Cache.Find(this);
if (Found)
{
return *Found;
}
// Not cached: calculate
FSourceDataDescriptor Result;
for (const FCase& Case : Cases)
{
if (Case.Branch)
{
FSourceDataDescriptor SourceDesc = Case.Branch->GetSourceDataDescriptor(Context);
Result.CombineWith(SourceDesc);
}
}
Context->Cache.Add(this, Result);
return Result;
}
}