// Copyright Epic Games, Inc. All Rights Reserved. #include "TextureBuildFunction.h" #include "DerivedDataCache.h" #include "DerivedDataPayload.h" #include "Engine/TextureDefines.h" #include "IImageWrapper.h" #include "IImageWrapperModule.h" #include "ImageCore.h" #include "Interfaces/ITextureFormat.h" #include "Interfaces/ITextureFormatModule.h" #include "Memory/CompositeBuffer.h" #include "Memory/SharedBuffer.h" #include "Modules/ModuleManager.h" #include "PixelFormat.h" #include "Serialization/CompactBinary.h" #include "Serialization/FileRegions.h" #include "Serialization/MemoryWriter.h" #include "TextureCompressorModule.h" #include "TextureFormatManager.h" DEFINE_LOG_CATEGORY_STATIC(LogTextureBuildFunction, Log, All); // Any edits to the texture compressor or this file that will change the output of texture builds // MUST have a corresponding change to this version. Individual texture formats have a version to // change that is specific to the format. A merge conflict affecting the version MUST be resolved // by generating a new version. static const FGuid TextureDerivedDataVersion(TEXT("a24fc8e0-42cb-49e8-bcd2-c8c4aa064bbd")); #ifndef CASE_ENUM_TO_TEXT #define CASE_ENUM_TO_TEXT(txt) case txt: return TEXT(#txt); #endif static const TCHAR* GetPixelFormatString(EPixelFormat InPixelFormat) { switch (InPixelFormat) { FOREACH_ENUM_EPIXELFORMAT(CASE_ENUM_TO_TEXT) default: return TEXT("PF_Unknown"); } } static void ReadCbField(FCbFieldView Field, bool& OutValue) { OutValue = Field.AsBool(OutValue); } static void ReadCbField(FCbFieldView Field, int32& OutValue) { OutValue = Field.AsInt32(OutValue); } static void ReadCbField(FCbFieldView Field, uint8& OutValue) { OutValue = Field.AsUInt8(OutValue); } static void ReadCbField(FCbFieldView Field, uint32& OutValue) { OutValue = Field.AsUInt32(OutValue); } static void ReadCbField(FCbFieldView Field, float& OutValue) { OutValue = Field.AsFloat(OutValue); } static void ReadCbField(FCbFieldView Field, FName& OutValue) { if (Field.IsString()) { OutValue = FName(FUTF8ToTCHAR(Field.AsString())); } } static void ReadCbField(FCbFieldView Field, FColor& OutValue) { FCbFieldViewIterator It = Field.AsArrayView().CreateViewIterator(); OutValue.A = It++->AsUInt8(OutValue.A); OutValue.R = It++->AsUInt8(OutValue.R); OutValue.G = It++->AsUInt8(OutValue.G); OutValue.B = It++->AsUInt8(OutValue.B); } static void ReadCbField(FCbFieldView Field, FVector4& OutValue) { FCbFieldViewIterator It = Field.AsArrayView().CreateViewIterator(); OutValue.X = It++->AsFloat(OutValue.X); OutValue.Y = It++->AsFloat(OutValue.Y); OutValue.Z = It++->AsFloat(OutValue.Z); OutValue.W = It++->AsFloat(OutValue.W); } static void ReadCbField(FCbFieldView Field, FIntPoint& OutValue) { FCbFieldViewIterator It = Field.AsArrayView().CreateViewIterator(); OutValue.X = It++->AsInt32(OutValue.X); OutValue.Y = It++->AsInt32(OutValue.Y); } static FTextureBuildSettings ReadBuildSettingsFromCompactBinary(const FCbObjectView& Object) { FTextureBuildSettings BuildSettings; BuildSettings.FormatConfigOverride = Object["FormatConfigOverride"].AsObjectView(); FCbObjectView ColorAdjustmentCbObj = Object["ColorAdjustment"].AsObjectView(); FColorAdjustmentParameters& ColorAdjustment = BuildSettings.ColorAdjustment; ReadCbField(ColorAdjustmentCbObj["AdjustBrightness"], ColorAdjustment.AdjustBrightness); ReadCbField(ColorAdjustmentCbObj["AdjustBrightnessCurve"], ColorAdjustment.AdjustBrightnessCurve); ReadCbField(ColorAdjustmentCbObj["AdjustSaturation"], ColorAdjustment.AdjustSaturation); ReadCbField(ColorAdjustmentCbObj["AdjustVibrance"], ColorAdjustment.AdjustVibrance); ReadCbField(ColorAdjustmentCbObj["AdjustRGBCurve"], ColorAdjustment.AdjustRGBCurve); ReadCbField(ColorAdjustmentCbObj["AdjustHue"], ColorAdjustment.AdjustHue); ReadCbField(ColorAdjustmentCbObj["AdjustMinAlpha"], ColorAdjustment.AdjustMinAlpha); ReadCbField(ColorAdjustmentCbObj["AdjustMaxAlpha"], ColorAdjustment.AdjustMaxAlpha); ReadCbField(Object["AlphaCoverageThresholds"], BuildSettings.AlphaCoverageThresholds); ReadCbField(Object["MipSharpening"], BuildSettings.MipSharpening); ReadCbField(Object["DiffuseConvolveMipLevel"], BuildSettings.DiffuseConvolveMipLevel); ReadCbField(Object["SharpenMipKernelSize"], BuildSettings.SharpenMipKernelSize); ReadCbField(Object["MaxTextureResolution"], BuildSettings.MaxTextureResolution); ReadCbField(Object["TextureFormatName"], BuildSettings.TextureFormatName); ReadCbField(Object["bHDRSource"], BuildSettings.bHDRSource); ReadCbField(Object["MipGenSettings"], BuildSettings.MipGenSettings); BuildSettings.bCubemap = Object["bCubemap"].AsBool(BuildSettings.bCubemap); BuildSettings.bTextureArray = Object["bTextureArray"].AsBool(BuildSettings.bTextureArray); BuildSettings.bVolume = Object["bVolume"].AsBool(BuildSettings.bVolume); BuildSettings.bLongLatSource = Object["bLongLatSource"].AsBool(BuildSettings.bLongLatSource); BuildSettings.bSRGB = Object["bSRGB"].AsBool(BuildSettings.bSRGB); BuildSettings.bUseLegacyGamma = Object["bUseLegacyGamma"].AsBool(BuildSettings.bUseLegacyGamma); BuildSettings.bPreserveBorder = Object["bPreserveBorder"].AsBool(BuildSettings.bPreserveBorder); BuildSettings.bForceNoAlphaChannel = Object["bForceNoAlphaChannel"].AsBool(BuildSettings.bForceNoAlphaChannel); BuildSettings.bForceAlphaChannel = Object["bForceAlphaChannel"].AsBool(BuildSettings.bForceAlphaChannel); BuildSettings.bDitherMipMapAlpha = Object["bDitherMipMapAlpha"].AsBool(BuildSettings.bDitherMipMapAlpha); BuildSettings.bComputeBokehAlpha = Object["bComputeBokehAlpha"].AsBool(BuildSettings.bComputeBokehAlpha); BuildSettings.bReplicateRed = Object["bReplicateRed"].AsBool(BuildSettings.bReplicateRed); BuildSettings.bReplicateAlpha = Object["bReplicateAlpha"].AsBool(BuildSettings.bReplicateAlpha); BuildSettings.bDownsampleWithAverage = Object["bDownsampleWithAverage"].AsBool(BuildSettings.bDownsampleWithAverage); BuildSettings.bSharpenWithoutColorShift = Object["bSharpenWithoutColorShift"].AsBool(BuildSettings.bSharpenWithoutColorShift); BuildSettings.bBorderColorBlack = Object["bBorderColorBlack"].AsBool(BuildSettings.bBorderColorBlack); BuildSettings.bFlipGreenChannel = Object["bFlipGreenChannel"].AsBool(BuildSettings.bFlipGreenChannel); BuildSettings.bApplyYCoCgBlockScale = Object["bApplyYCoCgBlockScale"].AsBool(BuildSettings.bApplyYCoCgBlockScale); BuildSettings.bApplyKernelToTopMip = Object["bApplyKernelToTopMip"].AsBool(BuildSettings.bApplyKernelToTopMip); BuildSettings.bRenormalizeTopMip = Object["bRenormalizeTopMip"].AsBool(BuildSettings.bRenormalizeTopMip); ReadCbField(Object["CompositeTextureMode"], BuildSettings.CompositeTextureMode); ReadCbField(Object["CompositePower"], BuildSettings.CompositePower); ReadCbField(Object["LODBias"], BuildSettings.LODBias); ReadCbField(Object["LODBiasWithCinematicMips"], BuildSettings.LODBiasWithCinematicMips); ReadCbField(Object["TopMipSize"], BuildSettings.TopMipSize); ReadCbField(Object["VolumeSizeZ"], BuildSettings.VolumeSizeZ); ReadCbField(Object["ArraySlices"], BuildSettings.ArraySlices); BuildSettings.bStreamable = Object["bStreamable"].AsBool(BuildSettings.bStreamable); BuildSettings.bVirtualStreamable = Object["bVirtualStreamable"].AsBool(BuildSettings.bVirtualStreamable); BuildSettings.bChromaKeyTexture = Object["bChromaKeyTexture"].AsBool(BuildSettings.bChromaKeyTexture); ReadCbField(Object["PowerOfTwoMode"], BuildSettings.PowerOfTwoMode); ReadCbField(Object["PaddingColor"], BuildSettings.PaddingColor); ReadCbField(Object["ChromaKeyColor"], BuildSettings.ChromaKeyColor); ReadCbField(Object["ChromaKeyThreshold"], BuildSettings.ChromaKeyThreshold); ReadCbField(Object["CompressionQuality"], BuildSettings.CompressionQuality); ReadCbField(Object["LossyCompressionAmount"], BuildSettings.LossyCompressionAmount); ReadCbField(Object["Downscale"], BuildSettings.Downscale); ReadCbField(Object["DownscaleOptions"], BuildSettings.DownscaleOptions); ReadCbField(Object["VirtualAddressingModeX"], BuildSettings.VirtualAddressingModeX); ReadCbField(Object["VirtualAddressingModeY"], BuildSettings.VirtualAddressingModeY); ReadCbField(Object["VirtualTextureTileSize"], BuildSettings.VirtualTextureTileSize); ReadCbField(Object["VirtualTextureBorderSize"], BuildSettings.VirtualTextureBorderSize); BuildSettings.bVirtualTextureEnableCompressZlib = Object["bVirtualTextureEnableCompressZlib"].AsBool(BuildSettings.bVirtualTextureEnableCompressZlib); BuildSettings.bVirtualTextureEnableCompressCrunch = Object["bVirtualTextureEnableCompressCrunch"].AsBool(BuildSettings.bVirtualTextureEnableCompressCrunch); BuildSettings.bHasEditorOnlyData = Object["bHasEditorOnlyData"].AsBool(BuildSettings.bHasEditorOnlyData); return BuildSettings; } static void ReadOutputSettingsFromCompactBinary(const FCbObjectView& Object, int32& NumInlineMips, FString& MipKeyPrefix) { NumInlineMips = Object["NumInlineMips"].AsInt32(); MipKeyPrefix = FUTF8ToTCHAR(Object["MipKeyPrefix"].AsString()); } static ERawImageFormat::Type ComputeRawImageFormat(ETextureSourceFormat SourceFormat) { switch (SourceFormat) { case TSF_G8: return ERawImageFormat::G8; case TSF_G16: return ERawImageFormat::G16; case TSF_BGRA8: return ERawImageFormat::BGRA8; case TSF_BGRE8: return ERawImageFormat::BGRE8; case TSF_RGBA16: return ERawImageFormat::RGBA16; case TSF_RGBA16F: return ERawImageFormat::RGBA16F; default: checkf(false, TEXT("Invalid source texture format encountered: %d."), SourceFormat); return ERawImageFormat::G8; } } static bool TryReadTextureSourceFromCompactBinary(FCbFieldView Source, UE::DerivedData::FBuildContext& Context, TArray& OutMips) { FSharedBuffer InputBuffer = Context.FindInput(FUTF8ToTCHAR(Source.GetName())); if (!InputBuffer) { UE_LOG(LogTextureBuildFunction, Error, TEXT("Missing input '%s'."), *WriteToString<64>(FUTF8ToTCHAR(Source.GetName()))); return false; } ETextureSourceCompressionFormat CompressionFormat = (ETextureSourceCompressionFormat)Source["CompressionFormat"].AsUInt8(); ETextureSourceFormat SourceFormat = (ETextureSourceFormat)Source["SourceFormat"].AsUInt8(); ERawImageFormat::Type RawImageFormat = ComputeRawImageFormat(SourceFormat); EGammaSpace GammaSpace = (EGammaSpace)Source["GammaSpace"].AsUInt8(); int32 NumSlices = Source["NumSlices"].AsInt32(); int32 SizeX = Source["SizeX"].AsInt32(); int32 SizeY = Source["SizeY"].AsInt32(); int32 MipSizeX = SizeX; int32 MipSizeY = SizeY; const uint8* DecompressedSourceData = (const uint8*)InputBuffer.GetData(); TArray64 IntermediateDecompressedData; if (CompressionFormat != TSCF_None) { switch (CompressionFormat) { case TSCF_JPEG: { TSharedPtr ImageWrapper = FModuleManager::GetModuleChecked(FName("ImageWrapper")).CreateImageWrapper(EImageFormat::JPEG); ImageWrapper->SetCompressed((const uint8*)InputBuffer.GetData(), InputBuffer.GetSize()); ImageWrapper->GetRaw(SourceFormat == TSF_G8 ? ERGBFormat::Gray : ERGBFormat::BGRA, 8, IntermediateDecompressedData); } break; case TSCF_PNG: { TSharedPtr ImageWrapper = FModuleManager::GetModuleChecked(FName("ImageWrapper")).CreateImageWrapper(EImageFormat::PNG); ImageWrapper->SetCompressed((const uint8*)InputBuffer.GetData(), InputBuffer.GetSize()); ERGBFormat RawFormat = (SourceFormat == TSF_G8 || SourceFormat == TSF_G16) ? ERGBFormat::Gray : ERGBFormat::RGBA; ImageWrapper->GetRaw(RawFormat, (SourceFormat == TSF_G16 || SourceFormat == TSF_RGBA16) ? 16 : 8, IntermediateDecompressedData); } break; default: UE_LOG(LogTextureBuildFunction, Error, TEXT("Unexpected source compression format encountered while attempting to build a texture.")); return false; } DecompressedSourceData = IntermediateDecompressedData.GetData(); InputBuffer.Reset(); } FCbArrayView MipsCbArrayView = Source["Mips"].AsArrayView(); OutMips.Reserve(MipsCbArrayView.Num()); for (FCbFieldView MipsCbArrayIt : MipsCbArrayView) { FCbObjectView MipCbObjectView = MipsCbArrayIt.AsObjectView(); int64 MipOffset = MipCbObjectView["Offset"].AsInt64(); int64 MipSize = MipCbObjectView["Size"].AsInt64(); FImage* SourceMip = new(OutMips) FImage( MipSizeX, MipSizeY, NumSlices, RawImageFormat, GammaSpace ); if ((MipsCbArrayView.Num() == 1) && (CompressionFormat != TSCF_None)) { // In the case where there is only one mip and its already in a TArray, there is no need to allocate new array contents, just use a move instead SourceMip->RawData = MoveTemp(IntermediateDecompressedData); } else { SourceMip->RawData.AddUninitialized(MipSize); FMemory::Memcpy( SourceMip->RawData.GetData(), DecompressedSourceData + MipOffset, MipSize ); } MipSizeX = FMath::Max(MipSizeX / 2, 1); MipSizeY = FMath::Max(MipSizeY / 2, 1); } return true; } FGuid FTextureBuildFunction::GetVersion() const { UE::DerivedData::FBuildVersionBuilder Builder; Builder << TextureDerivedDataVersion; GetVersion(Builder); return Builder.Build(); } void FTextureBuildFunction::Configure(UE::DerivedData::FBuildConfigContext& Context) const { Context.SetCacheBucket(Context.CreateCacheBucket(TEXT("Texture"_SV))); } void FTextureBuildFunction::Build(UE::DerivedData::FBuildContext& Context) const { const FCbObject Settings = Context.FindConstant(TEXT("Settings"_SV)); if (!Settings) { UE_LOG(LogTextureBuildFunction, Error, TEXT("Settings are not available.")); return; } const FTextureBuildSettings BuildSettings = ReadBuildSettingsFromCompactBinary(Settings["Build"].AsObjectView()); const uint16 RequiredTextureFormatVersion = Settings["FormatVersion"].AsUInt16(); if (ITextureFormatManagerModule* TFM = GetTextureFormatManager()) { const ITextureFormat* TextureFormat = TFM->FindTextureFormat(BuildSettings.TextureFormatName); const uint16 CurrentTextureFormatVersion = TextureFormat ? TextureFormat->GetVersion(BuildSettings.TextureFormatName, &BuildSettings) : 0; if (CurrentTextureFormatVersion != RequiredTextureFormatVersion) { UE_LOG(LogTextureBuildFunction, Error, TEXT("%s has version %hu when version %hu is required."), *BuildSettings.TextureFormatName.ToString(), CurrentTextureFormatVersion, RequiredTextureFormatVersion);; return; } } int32 NumInlineMips; FString MipKeyPrefix; ReadOutputSettingsFromCompactBinary(Settings["Output"].AsObjectView(), NumInlineMips, MipKeyPrefix); TArray SourceMips; if (!TryReadTextureSourceFromCompactBinary(Settings["Source"], Context, SourceMips)) { return; } TArray AssociatedNormalSourceMips; if (FCbFieldView CompositeSource = Settings["CompositeSource"]; CompositeSource && !TryReadTextureSourceFromCompactBinary(CompositeSource, Context, AssociatedNormalSourceMips)) { return; } UE_LOG(LogTextureBuildFunction, Display, TEXT("Compressing %d source mip(s) (%dx%d) to %s..."), SourceMips.Num(), SourceMips[0].SizeX, SourceMips[0].SizeY, *BuildSettings.TextureFormatName.ToString()); TArray CompressedMips; uint32 NumMipsInTail; uint32 ExtData; bool bBuildSucceeded = FModuleManager::GetModuleChecked(TEXTURE_COMPRESSOR_MODULENAME).BuildTexture( SourceMips, AssociatedNormalSourceMips, BuildSettings, CompressedMips, NumMipsInTail, ExtData); if (!bBuildSucceeded) { return; } check(CompressedMips.Num() > 0); // Write out streaming mips as payloads. int32 MipCount = CompressedMips.Num(); const bool bForceAllMipsToBeInlined = BuildSettings.bCubemap || (BuildSettings.bVolume && !BuildSettings.bStreamable) || (BuildSettings.bTextureArray && !BuildSettings.bStreamable); const int32 FirstInlineMip = bForceAllMipsToBeInlined ? 0 : FMath::Max(0, MipCount - FMath::Max(NumInlineMips, (int32)NumMipsInTail)); const int32 WritableMipCount = MipCount - ((NumMipsInTail > 0) ? ((int32)NumMipsInTail - 1) : 0); for (int32 MipIndex = 0; MipIndex < WritableMipCount; ++MipIndex) { if (MipIndex < FirstInlineMip) { const FCompressedImage2D& CompressedMip = CompressedMips[MipIndex]; TStringBuilder<32> PayloadName; PayloadName << "Mip" << MipIndex; int32 MipSize = CompressedMip.RawData.Num(); FSharedBuffer MipHeader = FSharedBuffer::MakeView(&MipSize, sizeof(int32)); FCompositeBuffer CompositeMipBuffer(MipHeader, FSharedBuffer::MakeView(CompressedMips[MipIndex].RawData.GetData(), CompressedMips[MipIndex].RawData.Num())); Context.AddPayload(UE::DerivedData::FPayloadId::FromName(*PayloadName), FCompressedBuffer::Compress(NAME_Default, CompositeMipBuffer)); } } // Write out texture platform data and non-streaming mip tail as a single payload // This is meant to match the behavior in SerializePlatformData in TextureDerivedData.cpp. // TODO: The texture header/footer information here should not be serialized from the worker but could be left to the editor to fill in. // Doing it here causes more code duplication than I would like. { bool bHasOptData = (NumMipsInTail != 0) || (ExtData != 0); TArray OrderedBuffers; TArray TextureHeaderArray; FMemoryWriter TextureHeaderWriter(TextureHeaderArray, /*bIsPersistent=*/ true); TextureHeaderWriter << CompressedMips[0].SizeX; TextureHeaderWriter << CompressedMips[0].SizeY; const int32 NumSlices = BuildSettings.bCubemap ? 6 : (BuildSettings.bVolume || BuildSettings.bTextureArray) ? CompressedMips[0].SizeZ : 1; static constexpr uint32 BitMask_CubeMap = 1u << 31u; static constexpr uint32 BitMask_HasOptData = 1u << 30u; static constexpr uint32 BitMask_NumSlices = BitMask_HasOptData - 1u; uint32 PackedData = (NumSlices & BitMask_NumSlices) | (BuildSettings.bCubemap ? BitMask_CubeMap : 0) | (bHasOptData ? BitMask_HasOptData : 0); TextureHeaderWriter << PackedData; FString PixelFormatString(GetPixelFormatString((EPixelFormat)CompressedMips[0].PixelFormat)); TextureHeaderWriter << PixelFormatString; if (bHasOptData) { TextureHeaderWriter << ExtData; TextureHeaderWriter << NumMipsInTail; } TextureHeaderWriter << MipCount; OrderedBuffers.Add(FSharedBuffer::MakeView(TextureHeaderArray.GetData(), TextureHeaderArray.Num())); TArray> MipHeaderArray; MipHeaderArray.AddDefaulted(MipCount); TArray> MipFooterArray; MipFooterArray.AddDefaulted(MipCount); int64 CurrentOffset = TextureHeaderArray.Num(); for (int32 MipIndex = 0; MipIndex < MipCount; ++MipIndex) { bool bIsInlineMip = MipIndex >= FirstInlineMip; FCompressedImage2D& CompressedMip = CompressedMips[MipIndex]; FMemoryWriter MipHeaderWriter(MipHeaderArray[MipIndex], /*bIsPersistent=*/ true); bool bCooked = false; MipHeaderWriter << bCooked; uint32 BulkDataFlags = 0; MipHeaderWriter << BulkDataFlags; int32 BulkDataElementCount = bIsInlineMip ? CompressedMip.RawData.Num() : 0; MipHeaderWriter << BulkDataElementCount; int32 BulkDataBytesOnDisk = bIsInlineMip ? CompressedMip.RawData.Num() : 0; MipHeaderWriter << BulkDataBytesOnDisk; int64 BulkDataOffset = CurrentOffset + MipHeaderArray[MipIndex].Num() + sizeof(int64); MipHeaderWriter << BulkDataOffset; OrderedBuffers.Add(FSharedBuffer::MakeView(MipHeaderArray[MipIndex].GetData(), MipHeaderArray[MipIndex].Num())); CurrentOffset += MipHeaderArray[MipIndex].Num(); if (bIsInlineMip) { OrderedBuffers.Add(FSharedBuffer::MakeView(CompressedMip.RawData.GetData(), CompressedMip.RawData.Num())); CurrentOffset += CompressedMip.RawData.Num(); } FMemoryWriter MipFooterWriter(MipFooterArray[MipIndex], /*bIsPersistent=*/ true); MipFooterWriter << CompressedMip.SizeX; MipFooterWriter << CompressedMip.SizeY; MipFooterWriter << CompressedMip.SizeZ; EFileRegionType FileRegion = FFileRegion::SelectType(EPixelFormat(CompressedMip.PixelFormat)); MipFooterWriter << FileRegion; TStringBuilder<512> DerivedDataKey; if (!bIsInlineMip) { // This has to match the behavior in GetTextureDerivedMipKey in TextureDerivedData.cpp DerivedDataKey << MipKeyPrefix; DerivedDataKey.Appendf(TEXT("_MIP%u_%dx%d"), MipIndex, CompressedMip.SizeX, CompressedMip.SizeY); } FString DerivedDataKeyFinal (DerivedDataKey.ToString()); MipFooterWriter << DerivedDataKeyFinal; OrderedBuffers.Add(FSharedBuffer::MakeView(MipFooterArray[MipIndex].GetData(), MipFooterArray[MipIndex].Num())); CurrentOffset += MipFooterArray[MipIndex].Num(); } // Bool serialized as 32-bit int is the footer uint32 IsVirtual = 0; OrderedBuffers.Add(FSharedBuffer::MakeView(&IsVirtual, sizeof(IsVirtual))); FCompositeBuffer CompositeResult(OrderedBuffers); Context.AddPayload(UE::DerivedData::FPayloadId::FromName(TEXT("Texture"_SV)), FCompressedBuffer::Compress(NAME_Default, CompositeResult)); } }