// Copyright Epic Games, Inc. All Rights Reserved. #include "Containers/SharedString.h" #include "GenericPlatform/GenericPlatformStackWalk.h" #include "Misc/DefinePrivateMemberPtr.h" #include "Misc/Paths.h" #include "ImageCore.h" #include "Modules/ModuleManager.h" #include "Interfaces/ITextureFormat.h" #include "Interfaces/ITextureFormatModule.h" #include "TextureCompressorModule.h" #include "PixelFormat.h" #include "HAL/PlatformProcess.h" #include "TextureBuildFunction.h" #include "DerivedDataBuildFunctionFactory.h" #ifndef __APPLE__ #define __APPLE__ 0 #endif #ifndef __unix__ #define __unix__ 0 #endif #include "Etc.h" #include "EtcErrorMetric.h" #include "EtcBlock4x4.h" #include "EtcImage.h" // Workaround for: error LNK2019: unresolved external symbol __imp___std_init_once_begin_initialize referenced in function "void __cdecl std::call_once // https://developercommunity.visualstudio.com/t/-imp-std-init-once-complete-unresolved-external-sy/1684365 #if defined(_MSC_VER) && (_MSC_VER >= 1932) // Visual Studio 2022 version 17.2+ # pragma comment(linker, "/alternatename:__imp___std_init_once_complete=__imp_InitOnceComplete") # pragma comment(linker, "/alternatename:__imp___std_init_once_begin_initialize=__imp_InitOnceBeginInitialize") #endif DEFINE_LOG_CATEGORY_STATIC(LogTextureFormatETC2, Log, All); class FETC2TextureBuildFunction final : public FTextureBuildFunction { const UE::FUtf8SharedString& GetName() const final { static const UE::FUtf8SharedString Name(UTF8TEXTVIEW("ETC2Texture")); return Name; } void GetVersion(UE::DerivedData::FBuildVersionBuilder& Builder, ITextureFormat*& OutTextureFormatVersioning) const final { static FGuid Version(TEXT("af5192f4-351f-422f-b539-f6bd4abadfae")); Builder << Version; OutTextureFormatVersioning = FModuleManager::GetModuleChecked(TEXT("TextureFormatETC2")).GetTextureFormat(); } }; /** * Macro trickery for supported format names. */ #define ENUM_SUPPORTED_FORMATS(op) \ op(ETC2_RGB) \ op(ETC2_RGBA) \ op(ETC2_R11) \ op(ETC2_RG11) \ op(AutoETC2) #define DECL_FORMAT_NAME(FormatName) static FName GTextureFormatName##FormatName = FName(TEXT(#FormatName)); ENUM_SUPPORTED_FORMATS(DECL_FORMAT_NAME); #undef DECL_FORMAT_NAME #define DECL_FORMAT_NAME_ENTRY(FormatName) GTextureFormatName##FormatName , static FName GSupportedTextureFormatNames[] = { ENUM_SUPPORTED_FORMATS(DECL_FORMAT_NAME_ENTRY) }; #undef DECL_FORMAT_NAME_ENTRY #undef ENUM_SUPPORTED_FORMATS // note InSourceData is not const, can be mutated by sanitize static bool CompressImageUsingEtc2comp( FLinearColor * InSourceColors, EPixelFormat PixelFormat, int32 SizeX, int32 SizeY, int64 NumPixels, EGammaSpace TargetGammaSpace, TArray64& OutCompressedData) { using namespace Etc; Image::Format EtcFormat = Image::Format::UNKNOWN; switch (PixelFormat) { case PF_ETC2_RGB: EtcFormat = Image::Format::RGB8; break; case PF_ETC2_RGBA: EtcFormat = Image::Format::RGBA8; break; case PF_ETC2_R11_EAC: EtcFormat = Image::Format::R11; break; case PF_ETC2_RG11_EAC: EtcFormat = Image::Format::RG11; break; default: UE_LOG(LogTextureFormatETC2, Fatal, TEXT("Unsupported EPixelFormat for compression: %u"), (uint32)PixelFormat); return false; } // RGBA, REC709, NUMERIC will set RGB to 0 if all pixels in the block are transparent (A=0) const Etc::ErrorMetric EtcErrorMetric = Etc::RGBX; const float EtcEffort = ETCCOMP_DEFAULT_EFFORT_LEVEL; // threads used by etc2comp : const unsigned int MAX_JOBS = 8; const unsigned int NUM_JOBS = 8; // to run etc2comp synchronously : //const unsigned int MAX_JOBS = 0; //const unsigned int NUM_JOBS = 0; unsigned char* paucEncodingBits = nullptr; unsigned int uiEncodingBitsBytes = 0; unsigned int uiExtendedWidth = 0; unsigned int uiExtendedHeight = 0; int iEncodingTime_ms = 0; float* SourceData = &InSourceColors[0].Component(0); // InSourceData is a linear color, we need to feed float* data to the codec in a target color space TArray64 IntermediateData; if (TargetGammaSpace == EGammaSpace::sRGB) { IntermediateData.Reserve(NumPixels * 4); IntermediateData.AddUninitialized(NumPixels * 4); for (int64 Idx = 0; Idx < IntermediateData.Num(); Idx += 4) { const FLinearColor& LinColor = *(FLinearColor*)(SourceData + Idx); FColor Color = LinColor.ToFColorSRGB(); IntermediateData[Idx + 0] = Color.R / 255.f; IntermediateData[Idx + 1] = Color.G / 255.f; IntermediateData[Idx + 2] = Color.B / 255.f; IntermediateData[Idx + 3] = Color.A / 255.f; } SourceData = IntermediateData.GetData(); } else { int64 NumFloats = NumPixels * 4; for(int64 Idx =0 ;Idx < NumFloats;Idx++) { // sanitize inf and nan : float f = SourceData[Idx]; if ( f >= -FLT_MAX && f <= FLT_MAX ) { // finite, leave it // nans will fail all compares so not go in here } else if ( f > FLT_MAX ) { // +inf SourceData[Idx] = FLT_MAX; } else if ( f < -FLT_MAX ) { // -inf SourceData[Idx] = -FLT_MAX; } else { // nan SourceData[Idx] = 0.f; } //check( ! FMath::IsNaN( SourceData[Idx] ) ); } } Encode( SourceData, SizeX, SizeY, EtcFormat, EtcErrorMetric, EtcEffort, NUM_JOBS, MAX_JOBS, &paucEncodingBits, &uiEncodingBitsBytes, &uiExtendedWidth, &uiExtendedHeight, &iEncodingTime_ms ); OutCompressedData.SetNumUninitialized(uiEncodingBitsBytes); FMemory::Memcpy(OutCompressedData.GetData(), paucEncodingBits, uiEncodingBitsBytes); delete[] paucEncodingBits; return true; } UE_DEFINE_PRIVATE_MEMBER_PTR(Etc::Image::Format, GPrivateFormatPtr, Etc::Image, m_format); /** * ETC2 texture format handler. */ class FTextureFormatETC2 : public ITextureFormat { public: static FGuid GetDecodeBuildFunctionVersionGuid() { static FGuid Version(TEXT("B1C15A49-199A-4CD0-8F03-E19FB13292C2")); return Version; } static FUtf8StringView GetDecodeBuildFunctionNameStatic() { return UTF8TEXTVIEW("FDecodeTextureFormatETC2"); } virtual const FUtf8StringView GetDecodeBuildFunctionName() const override final { return GetDecodeBuildFunctionNameStatic(); } virtual bool AllowParallelBuild() const override { return true; } virtual uint16 GetVersion( FName Format, const struct FTextureBuildSettings* BuildSettings = nullptr ) const override { return 3; } virtual FName GetEncoderName(FName Format) const override { static const FName ETC2Name("ETC2"); return ETC2Name; } virtual void GetSupportedFormats(TArray& OutFormats) const override { OutFormats.Append(GSupportedTextureFormatNames, UE_ARRAY_COUNT(GSupportedTextureFormatNames)); } virtual EPixelFormat GetEncodedPixelFormat(const FTextureBuildSettings& BuildSettings, bool bImageHasAlphaChannel) const override { if (BuildSettings.TextureFormatName == GTextureFormatNameETC2_RGB || BuildSettings.TextureFormatName == GTextureFormatNameETC2_RGBA || BuildSettings.TextureFormatName == GTextureFormatNameAutoETC2 ) { if ( BuildSettings.TextureFormatName == GTextureFormatNameETC2_RGB || !bImageHasAlphaChannel ) { // even if Name was RGBA we still use the RGB profile if !bImageHasAlphaChannel // so that "Compress Without Alpha" can force us to opaque return PF_ETC2_RGB; } else { return PF_ETC2_RGBA; } } if (BuildSettings.TextureFormatName == GTextureFormatNameETC2_R11) { return PF_ETC2_R11_EAC; } else if (BuildSettings.TextureFormatName == GTextureFormatNameETC2_RG11) { return PF_ETC2_RG11_EAC; } UE_LOG(LogTextureFormatETC2, Fatal, TEXT("Unhandled texture format '%s' given to FTextureFormatAndroid::GetEncodedPixelFormat()"), *BuildSettings.TextureFormatName.ToString()); return PF_Unknown; } virtual bool CanDecodeFormat(EPixelFormat InPixelFormat) const { return IsETCBlockCompressedPixelFormat(InPixelFormat); } virtual bool DecodeImage(int32 InSizeX, int32 InSizeY, int32 InNumSlices, EPixelFormat InPixelFormat, bool bInSRGB, const FName& InTextureFormatName, FSharedBuffer InEncodedData, FImage& OutImage, FStringView InTextureName) const { Etc::Image::Format EtcFormat; switch (InPixelFormat) { case PF_ETC2_RGBA: EtcFormat = Etc::Image::Format::RGBA8; break; case PF_ETC2_RGB: EtcFormat = Etc::Image::Format::RGB8; break; case PF_ETC2_R11_EAC: EtcFormat = Etc::Image::Format::R11; break; case PF_ETC2_RG11_EAC: EtcFormat = Etc::Image::Format::RG11; break; default: check(0); return false; // should never get here because of CanDecodeFormat() } uint32 BytesPerBlock = GPixelFormats[InPixelFormat].BlockBytes; check(BytesPerBlock == 16 || BytesPerBlock == 8); uint64 PitchInPixels = InSizeX; uint64 NumBlocksX = (InSizeX + 3) >> 2; uint64 NumBlocksY = (InSizeY + 3) >> 2; uint64 NumSlices = InNumSlices; uint64 BytesPerSlice = BytesPerBlock * NumBlocksX * NumBlocksY; if (NumSlices * NumBlocksX * NumBlocksY * BytesPerBlock != InEncodedData.GetSize()) { UE_LOG(LogTextureFormatETC2, Error, TEXT("Can't decode ETC2 image: incorrect amount of encoded data for image size: %d x %d x %" UINT64_FMT ", expected %" UINT64_FMT " got %" UINT64_FMT), InSizeX, InSizeY, NumSlices, NumSlices * NumBlocksX * NumBlocksY * BytesPerBlock, InEncodedData.GetSize()); return false; } // Etc actually alters the source image based on format and actually looks at the bits so they have to be valid even if they aren't // representative. TArray64 GarbageSourceBits; GarbageSourceBits.AddUninitialized(InSizeX * InSizeY * sizeof(FLinearColor)); Etc::Image SourceImage((float*)GarbageSourceBits.GetData(), InSizeX, InSizeY, Etc::ErrorMetric::RGBA); // Annoyingly, there doesn't appear to be a way to set the image format during decoding - even using the encoded bits constructor with // the actual format parameter doesn't matter because the relevant assert is looking at the source image which has format unknown. So // we edit the header to make this member public: SourceImage.*GPrivateFormatPtr = EtcFormat; // this is the same as SourceImage.m_format = EtcFormat; // This is so we don't have to allocate a full sized full linear color image - we copy into a 4x4 image and then blit the bits // back out. FImage LinearImage(4, 4, 1, ERawImageFormat::RGBA32F, EGammaSpace::Linear); FImage FColorImage(4, 4, 1, ERawImageFormat::BGRA8, EGammaSpace::Linear); FLinearColor* LinearBlock = (FLinearColor*)LinearImage.RawData.GetData(); FColor* FColorBlock = (FColor*)FColorImage.RawData.GetData(); OutImage.Init(InSizeX, InSizeY, InNumSlices, ERawImageFormat::BGRA8, EGammaSpace::Linear); // \todo profile this and see if we can do anything or if we're just boned by the interface. for (uint64 Slice = 0; Slice < NumSlices; Slice++) { for (uint64 BlockY = 0; BlockY < NumBlocksY; BlockY++) { for (uint64 BlockX = 0; BlockX < NumBlocksX; BlockX++) { uint64 BlockOffset = BytesPerBlock * (BlockY * NumBlocksX + BlockX) + BytesPerSlice * Slice; if (BlockOffset + BytesPerBlock > InEncodedData.GetSize()) { UE_LOG(LogTextureFormatETC2, Error, TEXT("Invalid block offset calculated during DecodeImage: %llu + %d, have %llu bytes available. Texture %.*s"), BlockOffset, BytesPerBlock, InEncodedData.GetSize(), InTextureName.Len(), InTextureName.GetData()); UE_LOG(LogTextureFormatETC2, Error, TEXT("....Slice %llu BlockX %llu BlockY %llu NumBlocksX %llu NumBlocksY %llu BytesPerSlice %llu"), Slice, BlockX, BlockY, NumBlocksX, NumBlocksY, BytesPerSlice); return false; } const uint8* BlockBits = (uint8*)InEncodedData.GetData() + BlockOffset; Etc::Block4x4 Block; Block.InitFromEtcEncodingBits(EtcFormat, BlockX * 4, BlockY * 4, (uint8*)BlockBits, &SourceImage, Etc::ErrorMetric::RGBA); // Decode the color into a small 4x4 linear block Etc::ColorFloatRGBA* DecodedColors = Block.GetDecodedColors(); for (uint64 PixelX = 0; PixelX < 4; PixelX++) { for (uint64 PixelY = 0; PixelY < 4; PixelY++) { LinearBlock[PixelY * 4 + PixelX].R = DecodedColors[PixelX * 4 + PixelY].fR; LinearBlock[PixelY * 4 + PixelX].G = DecodedColors[PixelX * 4 + PixelY].fG; LinearBlock[PixelY * 4 + PixelX].B = DecodedColors[PixelX * 4 + PixelY].fB; LinearBlock[PixelY * 4 + PixelX].A = 1.0f; } } if (InPixelFormat == PF_ETC2_RGBA || InPixelFormat == PF_ETC2_RG11_EAC) // could have punchthrough alpha { float* DecodedAlphas = Block.GetDecodedAlphas(); for (uint64 PixelX = 0; PixelX < 4; PixelX++) { for (uint64 PixelY = 0; PixelY < 4; PixelY++) { LinearBlock[PixelY * 4 + PixelX].A = DecodedAlphas[PixelX * 4 + PixelY]; } } } // Convert to our output format LinearImage.CopyTo(FColorImage, ERawImageFormat::BGRA8, EGammaSpace::Linear); // Now copy these bits in to the actual output image { FColor* OutputBlock = (FColor*)OutImage.GetPixelPointer(BlockX * 4, BlockY * 4, Slice); uint64 BlockPixelsW = 4; uint64 BlockPixelsH = 4; if (BlockX == NumBlocksX - 1) { BlockPixelsW = InSizeX - BlockX * 4; } if (BlockY == NumBlocksY - 1) { BlockPixelsH = InSizeY - BlockY * 4; } for (uint64 PixelY = 0; PixelY < BlockPixelsH; PixelY++) { FMemory::Memcpy(OutputBlock + PixelY * PitchInPixels, FColorBlock + PixelY * 4, sizeof(FColor)*BlockPixelsW); } } // end copy to output } // end each horiz block } // end each vert block } // end each slice return true; } virtual bool CompressImage( const FImage& InImage, const struct FTextureBuildSettings& BuildSettings, const FIntVector3& InMip0Dimensions, int32 InMip0NumSlicesNoDepth, int32 InMipIndex, int32 InMipCount, FStringView DebugTexturePathName, bool bImageHasAlphaChannel, FCompressedImage2D& OutCompressedImage ) const override { const FImage& Image = InImage; // Source is expected to be F32 linear color check(Image.Format == ERawImageFormat::RGBA32F); EPixelFormat CompressedPixelFormat = GetEncodedPixelFormat(BuildSettings, bImageHasAlphaChannel); bool bCompressionSucceeded = true; int64 SliceSize = Image.GetSliceNumPixels(); for (int32 SliceIndex = 0; SliceIndex < Image.NumSlices && bCompressionSucceeded; ++SliceIndex) { TArray64 CompressedSliceData; const FLinearColor * SlicePixels = Image.AsRGBA32F().GetData() + SliceIndex * SliceSize; bCompressionSucceeded = CompressImageUsingEtc2comp( const_cast(SlicePixels), CompressedPixelFormat, Image.SizeX, Image.SizeY, SliceSize, BuildSettings.GetDestGammaSpace(), CompressedSliceData ); OutCompressedImage.RawData.Append(CompressedSliceData); } if (bCompressionSucceeded) { OutCompressedImage.SizeX = Image.SizeX; OutCompressedImage.SizeY = Image.SizeY; OutCompressedImage.NumSlicesWithDepth = Image.NumSlices; OutCompressedImage.PixelFormat = CompressedPixelFormat; } return bCompressionSucceeded; } }; class FTextureFormatETC2Module : public ITextureFormatModule { public: ITextureFormat* Singleton = NULL; FTextureFormatETC2Module() { } virtual ~FTextureFormatETC2Module() { if ( Singleton ) { delete Singleton; Singleton = nullptr; } } virtual void StartupModule() override { } virtual bool CanCallGetTextureFormats() override { return false; } virtual ITextureFormat* GetTextureFormat() { if ( Singleton == nullptr ) // not thread safe { FTextureFormatETC2* ptr = new FTextureFormatETC2(); Singleton = ptr; } return Singleton; } static inline UE::DerivedData::TBuildFunctionFactory BuildFunctionFactory; static inline UE::DerivedData::TBuildFunctionFactory> DecodeBuildFunctionFactory; }; IMPLEMENT_MODULE(FTextureFormatETC2Module, TextureFormatETC2);