// Copyright Epic Games, Inc. All Rights Reserved. #include "MuT/ASTOpImageMipmap.h" #include "Containers/Map.h" #include "HAL/UnrealMemory.h" #include "Math/UnrealMathSSE.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" #include "MuT/ASTOpConditional.h" #include "MuT/ASTOpImageCompose.h" #include "MuT/ASTOpImagePatch.h" #include "MuT/ASTOpImagePixelFormat.h" #include "MuT/ASTOpImageBlankLayout.h" #include "MuT/ASTOpImagePlainColor.h" #include "MuT/ASTOpSwitch.h" namespace mu { ASTOpImageMipmap::ASTOpImageMipmap() : Source(this) { } ASTOpImageMipmap::~ASTOpImageMipmap() { // Explicit call needed to avoid recursive destruction ASTOp::RemoveChildren(); } bool ASTOpImageMipmap::IsEqual(const ASTOp& otherUntyped) const { if (otherUntyped.GetOpType()==GetOpType()) { const ASTOpImageMipmap* other = static_cast(&otherUntyped); return Source == other->Source && Levels == other->Levels && BlockLevels == other->BlockLevels && bOnlyTail == other->bOnlyTail && bPreventSplitTail == other->bPreventSplitTail && AddressMode == other->AddressMode && FilterType == other->FilterType; } return false; } uint64 ASTOpImageMipmap::Hash() const { uint64 res = std::hash()(Source.child().get()); hash_combine(res, Levels); return res; } mu::Ptr ASTOpImageMipmap::Clone(MapChildFuncRef mapChild) const { mu::Ptr n = new ASTOpImageMipmap(); n->Source = mapChild(Source.child()); n->Levels = Levels; n->BlockLevels = BlockLevels; n->bOnlyTail = bOnlyTail; n->bPreventSplitTail = bPreventSplitTail; n->AddressMode = AddressMode; n->FilterType = FilterType; return n; } void ASTOpImageMipmap::ForEachChild(const TFunctionRef f) { f(Source); } void ASTOpImageMipmap::Link(FProgram& program, FLinkerOptions*) { // Already linked? if (!linkedAddress) { OP::ImageMipmapArgs Args; FMemory::Memzero(Args); Args.levels = Levels; Args.blockLevels = BlockLevels; Args.onlyTail = bOnlyTail; Args.AddressMode = AddressMode; Args.FilterType = FilterType; if (Source) Args.source = Source->linkedAddress; linkedAddress = (OP::ADDRESS)program.OpAddress.Num(); //program.m_code.push_back(op); program.OpAddress.Add((uint32_t)program.ByteCode.Num()); AppendCode(program.ByteCode, GetOpType()); AppendCode(program.ByteCode, Args); } } mu::Ptr ASTOpImageMipmap::OptimiseSemantic(const FModelOptimizationOptions&, int32 Pass) const { mu::Ptr at; // \TODO: This seems to fail with the bandit test model. //if (Source.child()) //{ // ASTOp::FGetImageDescContext context; // FImageDesc ChildDesc = Source.child()->GetImageDesc(false, &context); // if (ChildDesc.m_lods>0 && ChildDesc.m_lods>=Levels) // { // // We can skip the mipmaps, because the child will always contain all lods anyway. // at = Source.child(); // } //} return at; } mu::Ptr ASTOpImageMipmap::OptimiseSink(const FModelOptimizationOptions&, FOptimizeSinkContext& context) const { mu::Ptr at; mu::Ptr sourceAt = Source.child(); switch (sourceAt->GetOpType()) { case EOpType::IM_BLANKLAYOUT: { // Set the mipmap generation on the blank layout operation Ptr NewOp = mu::Clone(sourceAt); NewOp->GenerateMipmaps = 1; // true NewOp->MipmapCount = Levels; at = NewOp; break; } case EOpType::IM_PLAINCOLOUR: { // Set the mipmap generation on the plaincolour operation Ptr mop = mu::Clone(sourceAt); mop->LODs = Levels; at = mop; break; } case EOpType::IM_PIXELFORMAT: { // Swap unless the mipmap operation builds only the tail or is compressed. // Otherwise, we could fall in a loop of swapping mipmaps and pixelformats. mu::Ptr mop = mu::Clone(this); mu::Ptr fop = mu::Clone(sourceAt); bool isCompressedFormat = IsCompressedFormat(fop->Format); if (isCompressedFormat && !mop->bOnlyTail) { mop->Source = fop->Source.child(); fop->Source = mop; at = fop; } break; } default: { at = context.ImageMipmapSinker.Apply(this); break; } // mipmap source default } // mipmap source type switch return at; } //! FImageDesc ASTOpImageMipmap::GetImageDesc(bool returnBestOption, FGetImageDescContext* context) const { FImageDesc res; // 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; } } if (Source.child()) { res = Source.child()->GetImageDesc(returnBestOption, context); } int mipLevels = Levels; if (mipLevels == 0) { mipLevels = FMath::Max( (int)ceilf(logf((float)res.m_size[0]) / logf(2.0f)), (int)ceilf(logf((float)res.m_size[1]) / logf(2.0f)) ); } res.m_lods = FMath::Max(res.m_lods, (uint8)mipLevels); // Cache the result if (context) { context->m_results.Add(this, res); } return res; } void ASTOpImageMipmap::GetLayoutBlockSize(int* pBlockX, int* pBlockY) { if (Source.child()) { // Assume the block size of the biggest mip Source.child()->GetLayoutBlockSize(pBlockX, pBlockY); } } bool ASTOpImageMipmap::IsImagePlainConstant(FVector4f& colour) const { bool res = false; if (Source.child()) { Source.child()->IsImagePlainConstant(colour); } return res; } mu::Ptr ASTOpImageMipmap::GetImageSizeExpression() const { mu::Ptr pRes; if (Source.child()) { pRes = Source.child()->GetImageSizeExpression(); } else { pRes = new ImageSizeExpression; } return pRes; } FSourceDataDescriptor ASTOpImageMipmap::GetSourceDataDescriptor(FGetSourceDataDescriptorContext* Context) const { if (Source) { return Source->GetSourceDataDescriptor(Context); } return {}; } //--------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------- mu::Ptr Sink_ImageMipmapAST::Apply(const ASTOpImageMipmap* InRoot) { Root = InRoot; OldToNew.Empty(); if (Root->bOnlyTail) { return nullptr; } mu::Ptr newSource; InitialSource = Root->Source.child(); // Before sinking, see if it is worth splitting into miptail and mip. // We always need to leave the root mipmap operation in case any intermediate operation fails to generate all mips. if (!Root->bPreventSplitTail) { // the block mipmaps can be done before composition is done. mu::Ptr newMip = mu::Clone(Root); newMip->Levels = Root->BlockLevels; newMip->BlockLevels = Root->BlockLevels; newMip->bOnlyTail = false; newMip->bPreventSplitTail = true; // the smallest mipmaps after the composition is done. mu::Ptr topMipOp = mu::Clone(Root); topMipOp->bOnlyTail = true; topMipOp->bPreventSplitTail = true; // Proceed topMipOp->Source = Visit(InitialSource, newMip.get()); newSource = topMipOp; } else { // Proceed newSource = Visit(InitialSource, Root); } Root = nullptr; // If there is any change, it is the new root. if (newSource != InitialSource) { return newSource; } return nullptr; } //--------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------- mu::Ptr Sink_ImageMipmapAST::Visit(mu::Ptr at, const ASTOpImageMipmap* currentMipmapOp) { if (!at) return nullptr; // Already visited? const Ptr* Cached = OldToNew.Find({ at,currentMipmapOp }); if (Cached) { return *Cached; } mu::Ptr newAt = at; switch (at->GetOpType()) { case EOpType::IM_CONDITIONAL: { // We move the op down the two paths Ptr newOp = mu::Clone(at); newOp->yes = Visit(newOp->yes.child(), currentMipmapOp); newOp->no = Visit(newOp->no.child(), currentMipmapOp); newAt = newOp; break; } case EOpType::IM_SWITCH: { // We move the op down all the paths Ptr newOp = mu::Clone(at); newOp->Default = Visit(newOp->Default.child(), currentMipmapOp); for (ASTOpSwitch::FCase& c : newOp->Cases) { c.Branch = Visit(c.Branch.child(), currentMipmapOp); } newAt = newOp; break; } case EOpType::IM_COMPOSE: { const ASTOpImageCompose* typedAt = static_cast(at.get()); if (!currentMipmapOp->bOnlyTail && // Don't move the mipmapping if we are composing with a mask. // TODO: allow mipmapping in the masks, RLE formats, etc. !typedAt->Mask ) { Ptr newOp = mu::Clone(at); Ptr baseOp = newOp->Base.child(); newOp->Base = Visit(baseOp, currentMipmapOp); Ptr blockOp = newOp->BlockImage.child(); newOp->BlockImage = Visit(blockOp, currentMipmapOp); newAt = newOp; } break; } case EOpType::IM_PATCH: { if (!currentMipmapOp->bOnlyTail) { // Special case: we propagate the mipmapping down the patch up to // the level allowed by the patch size and placement. We then leave // a top-level mipmapping operation to generate the smallest // mipmaps after the patch is done. const ASTOpImagePatch* typedSource = static_cast(at.get()); Ptr rectOp = typedSource->patch.child(); ASTOp::FGetImageDescContext context; FImageDesc patchDesc = rectOp->GetImageDesc(false, &context); // Calculate the mip levels that can be calculated for the patch uint8 PatchBlockLevels = 0; { uint16 minX = typedSource->location[0]; uint16 minY = typedSource->location[1]; uint16 sizeX = patchDesc.m_size[0]; uint16 sizeY = patchDesc.m_size[1]; while (minX && minY && sizeX && sizeY && minX % 2 == 0 && minY % 2 == 0 && sizeX % 2 == 0 && sizeY % 2 == 0) { PatchBlockLevels++; minX /= 2; minY /= 2; sizeX /= 2; sizeY /= 2; } } if (currentMipmapOp->Levels!= PatchBlockLevels || currentMipmapOp->BlockLevels!= PatchBlockLevels) { mu::Ptr newMip = mu::Clone(currentMipmapOp); newMip->Levels = PatchBlockLevels; newMip->BlockLevels = PatchBlockLevels; newMip->bOnlyTail = false; Ptr newOp = mu::Clone(at); newOp->base = Visit(newOp->base.child(), newMip.get()); newOp->patch = Visit(newOp->patch.child(), newMip.get()); newAt = newOp; if (currentMipmapOp->Levels != currentMipmapOp->BlockLevels // If the current levels are all of them, we will want to rebuild the mips after patch. // This happens if ignoring layouts, in which case there is no top-most mipmap to ensure the tail already. || currentMipmapOp->BlockLevels == 0 ) { // We need to add a mipmap on top to finish the mipmapping mu::Ptr topMipOp = mu::Clone(currentMipmapOp); topMipOp->Source = newOp; topMipOp->bOnlyTail = true; newAt = topMipOp; } } else { // The patch supports the same amount of mips that we are currently sinking. Ptr newOp = mu::Clone(at); newOp->base = Visit(newOp->base.child(), currentMipmapOp); newOp->patch = Visit(newOp->patch.child(), currentMipmapOp); newAt = newOp; } } break; } default: break; } // end on line, replace with mipmap if (at == newAt && at != InitialSource) { mu::Ptr newOp = mu::Clone(currentMipmapOp); check(newOp->GetOpType() == EOpType::IM_MIPMAP); newOp->Source = at; newAt = newOp; } OldToNew.Add({ at,currentMipmapOp }, newAt); return newAt; } }