2022-06-14 19:00:42 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
2024-06-21 12:52:32 -04:00
# include "Containers/SharedString.h"
2022-06-14 19:00:42 -04:00
# include "GenericPlatform/GenericPlatformStackWalk.h"
2024-04-19 16:57:52 -04:00
# include "Misc/DefinePrivateMemberPtr.h"
2022-06-14 19:00:42 -04:00
# 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
2022-09-13 21:48:33 -04:00
# include "Etc.h"
# include "EtcErrorMetric.h"
2024-04-19 16:57:52 -04:00
# include "EtcBlock4x4.h"
2022-09-13 21:48:33 -04:00
# include "EtcImage.h"
2022-06-14 19:00:42 -04:00
// 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
{
2024-06-21 12:52:32 -04:00
const UE : : FUtf8SharedString & GetName ( ) const final
2022-06-14 19:00:42 -04:00
{
2024-06-21 12:52:32 -04:00
static const UE : : FUtf8SharedString Name ( UTF8TEXTVIEW ( " ETC2Texture " ) ) ;
2022-06-14 19:00:42 -04:00
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 ) \
2023-02-13 10:02:24 -05:00
op ( ETC2_RG11 ) \
2022-06-14 19:00:42 -04:00
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
2023-01-13 18:29:33 -05:00
// note InSourceData is not const, can be mutated by sanitize
2022-06-14 19:00:42 -04:00
static bool CompressImageUsingEtc2comp (
2023-01-13 18:29:33 -05:00
FLinearColor * InSourceColors ,
2022-06-14 19:00:42 -04:00
EPixelFormat PixelFormat ,
int32 SizeX ,
int32 SizeY ,
2023-01-13 18:29:33 -05:00
int64 NumPixels ,
2022-06-14 19:00:42 -04:00
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 ;
2023-01-13 18:29:33 -05:00
// threads used by etc2comp :
2022-06-14 19:00:42 -04:00
const unsigned int MAX_JOBS = 8 ;
const unsigned int NUM_JOBS = 8 ;
2023-01-13 18:29:33 -05:00
// to run etc2comp synchronously :
//const unsigned int MAX_JOBS = 0;
//const unsigned int NUM_JOBS = 0;
2022-06-14 19:00:42 -04:00
unsigned char * paucEncodingBits = nullptr ;
unsigned int uiEncodingBitsBytes = 0 ;
unsigned int uiExtendedWidth = 0 ;
unsigned int uiExtendedHeight = 0 ;
int iEncodingTime_ms = 0 ;
2023-01-13 18:29:33 -05:00
float * SourceData = & InSourceColors [ 0 ] . Component ( 0 ) ;
2022-06-14 19:00:42 -04:00
// 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 ( ) ;
}
2023-01-13 18:29:33 -05:00
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] ) );
}
}
2022-06-14 19:00:42 -04:00
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 ;
}
2024-04-19 16:57:52 -04:00
UE_DEFINE_PRIVATE_MEMBER_PTR ( Etc : : Image : : Format , GPrivateFormatPtr , Etc : : Image , m_format ) ;
2022-06-14 19:00:42 -04:00
/**
* ETC2 texture format handler .
*/
class FTextureFormatETC2 : public ITextureFormat
{
public :
2024-04-19 16:57:52 -04:00
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 ( ) ;
}
2022-06-14 19:00:42 -04:00
virtual bool AllowParallelBuild ( ) const override
{
return true ;
}
virtual uint16 GetVersion (
FName Format ,
const struct FTextureBuildSettings * BuildSettings = nullptr
) const override
{
2023-02-13 10:02:24 -05:00
return 3 ;
2022-06-14 19:00:42 -04:00
}
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 ;
}
2023-02-13 10:02:24 -05:00
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameETC2_RG11 )
{
return PF_ETC2_RG11_EAC ;
}
2022-06-14 19:00:42 -04:00
UE_LOG ( LogTextureFormatETC2 , Fatal , TEXT ( " Unhandled texture format '%s' given to FTextureFormatAndroid::GetEncodedPixelFormat() " ) , * BuildSettings . TextureFormatName . ToString ( ) ) ;
return PF_Unknown ;
}
2024-04-19 16:57:52 -04:00
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 %d, expected %llu got %llu " ) ,
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 ;
}
2022-06-14 19:00:42 -04:00
virtual bool CompressImage (
2023-04-19 17:29:39 -04:00
const FImage & InImage ,
2022-06-14 19:00:42 -04:00
const struct FTextureBuildSettings & BuildSettings ,
2022-11-07 18:29:15 -05:00
const FIntVector3 & InMip0Dimensions ,
int32 InMip0NumSlicesNoDepth ,
2023-03-07 15:15:48 -05:00
int32 InMipIndex ,
int32 InMipCount ,
2022-06-14 19:00:42 -04:00
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 ;
2023-01-13 18:29:33 -05:00
int64 SliceSize = Image . GetSliceNumPixels ( ) ;
2022-06-14 19:00:42 -04:00
for ( int32 SliceIndex = 0 ; SliceIndex < Image . NumSlices & & bCompressionSucceeded ; + + SliceIndex )
{
TArray64 < uint8 > CompressedSliceData ;
2023-01-13 18:29:33 -05:00
const FLinearColor * SlicePixels = Image . AsRGBA32F ( ) . GetData ( ) + SliceIndex * SliceSize ;
2022-06-14 19:00:42 -04:00
bCompressionSucceeded = CompressImageUsingEtc2comp (
2023-01-13 18:29:33 -05:00
const_cast < FLinearColor * > ( SlicePixels ) ,
2022-06-14 19:00:42 -04:00
CompressedPixelFormat ,
Image . SizeX ,
Image . SizeY ,
2023-01-13 18:29:33 -05:00
SliceSize ,
2022-06-14 19:00:42 -04:00
BuildSettings . GetDestGammaSpace ( ) ,
CompressedSliceData
) ;
OutCompressedImage . RawData . Append ( CompressedSliceData ) ;
}
if ( bCompressionSucceeded )
{
OutCompressedImage . SizeX = Image . SizeX ;
OutCompressedImage . SizeY = Image . SizeY ;
2024-03-04 14:39:15 -05:00
OutCompressedImage . NumSlicesWithDepth = Image . NumSlices ;
2022-06-14 19:00:42 -04:00
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 ;
2024-04-19 16:57:52 -04:00
static inline UE : : DerivedData : : TBuildFunctionFactory < FGenericTextureDecodeBuildFunction < FTextureFormatETC2 > > DecodeBuildFunctionFactory ;
2022-06-14 19:00:42 -04:00
} ;
IMPLEMENT_MODULE ( FTextureFormatETC2Module , TextureFormatETC2 ) ;