Files
UnrealEngine/Engine/Source/Developer/TextureFormatETC2/Private/TextureFormatETC2.cpp
2025-05-18 13:04:45 +08:00

489 lines
16 KiB
C++

// 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<ITextureFormatModule>(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<uint8>& 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<float> 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<FName>& 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<uint8> 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<uint8> CompressedSliceData;
const FLinearColor * SlicePixels = Image.AsRGBA32F().GetData() + SliceIndex * SliceSize;
bCompressionSucceeded = CompressImageUsingEtc2comp(
const_cast<FLinearColor *>(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<FETC2TextureBuildFunction> BuildFunctionFactory;
static inline UE::DerivedData::TBuildFunctionFactory<FGenericTextureDecodeBuildFunction<FTextureFormatETC2>> DecodeBuildFunctionFactory;
};
IMPLEMENT_MODULE(FTextureFormatETC2Module, TextureFormatETC2);