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 "Misc/ScopeLock.h"
# include "Containers/IndirectArray.h"
# include "Stats/Stats.h"
# include "Async/AsyncWork.h"
# include "HAL/IConsoleManager.h"
# include "ImageCore.h"
# include "Modules/ModuleManager.h"
# include "Interfaces/ITextureFormat.h"
# include "Interfaces/ITextureFormatModule.h"
# include "TextureCompressorModule.h"
# include "PixelFormat.h"
# include "EngineLogs.h"
# include "Async/ParallelFor.h"
# include "TextureBuildFunction.h"
# include "DerivedDataBuildFunctionFactory.h"
# include "Misc/Paths.h"
THIRD_PARTY_INCLUDES_START
2022-09-13 21:48:33 -04:00
# include "nvtt/nvtt.h"
2022-06-14 19:00:42 -04:00
THIRD_PARTY_INCLUDES_END
DEFINE_LOG_CATEGORY_STATIC ( LogTextureFormatDXT , Log , All ) ;
class FDXTTextureBuildFunction 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 ( " DXTTexture " ) ) ;
2022-06-14 19:00:42 -04:00
return Name ;
}
void GetVersion ( UE : : DerivedData : : FBuildVersionBuilder & Builder , ITextureFormat * & OutTextureFormatVersioning ) const final
{
static FGuid Version ( TEXT ( " c2d5dbc5-131c-4525-a332-843230076d99 " ) ) ;
Builder < < Version ;
OutTextureFormatVersioning = FModuleManager : : GetModuleChecked < ITextureFormatModule > ( TEXT ( " TextureFormatDXT " ) ) . GetTextureFormat ( ) ;
}
} ;
/**
* Macro trickery for supported format names .
*/
# define ENUM_SUPPORTED_FORMATS(op) \
op ( DXT1 ) \
op ( DXT3 ) \
op ( DXT5 ) \
op ( AutoDXT ) \
op ( DXT5n ) \
op ( BC4 ) \
op ( BC5 )
# 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
/**
* NVTT output handler .
*/
struct FNVOutputHandler : public nvtt : : OutputHandler
{
2023-05-17 15:57:20 -04:00
explicit FNVOutputHandler ( uint8 * InBuffer , int64 InBufferSize )
2022-06-14 19:00:42 -04:00
: Buffer ( InBuffer )
, BufferEnd ( InBuffer + InBufferSize )
{
}
~ FNVOutputHandler ( )
{
}
virtual void beginImage ( int size , int width , int height , int depth , int face , int miplevel )
{
}
virtual bool writeData ( const void * data , int size )
{
check ( data ) ;
check ( Buffer + size < = BufferEnd ) ;
FMemory : : Memcpy ( Buffer , data , size ) ;
Buffer + = size ;
return true ;
}
virtual void endImage ( )
{
}
uint8 * Buffer ;
uint8 * BufferEnd ;
} ;
/**
* NVTT error handler .
*/
struct FNVErrorHandler : public nvtt : : ErrorHandler
{
FNVErrorHandler ( ) :
bSuccess ( true )
{ }
virtual void error ( nvtt : : Error e )
{
UE_LOG ( LogTextureFormatDXT , Warning , TEXT ( " nvtt::compress() failed with error '%s' " ) , ANSI_TO_TCHAR ( nvtt : : errorString ( e ) ) ) ;
bSuccess = false ;
}
bool bSuccess ;
} ;
/**
* All state objects needed for NVTT .
*/
class FNVTTCompressor
{
FNVOutputHandler OutputHandler ;
FNVErrorHandler ErrorHandler ;
nvtt : : InputOptions InputOptions ;
nvtt : : CompressionOptions CompressionOptions ;
nvtt : : OutputOptions OutputOptions ;
nvtt : : Compressor Compressor ;
public :
/** Initialization constructor. */
FNVTTCompressor (
const void * SourceData ,
EPixelFormat PixelFormat ,
int32 SizeX ,
int32 SizeY ,
bool bSRGB ,
bool bIsNormalMap ,
uint8 * OutBuffer ,
2023-05-17 15:57:20 -04:00
int64 BufferSize ,
2022-06-14 19:00:42 -04:00
bool bPreview = false )
: OutputHandler ( OutBuffer , BufferSize )
{
// CUDA acceleration currently disabled, needs more robust error handling
// With one core of a Xeon 3GHz CPU, compressing a 2048^2 normal map to DXT1 with NVTT 2.0.4 takes 7.49s.
// With the same settings but using CUDA and a Geforce 8800 GTX it takes 1.66s.
// To use CUDA, a CUDA 2.0 capable driver is required (178.08 or greater) and a Geforce 8 or higher.
const bool bUseCUDAAcceleration = false ;
// DXT1a support is currently not exposed.
const bool bSupportDXT1a = false ;
// Quality level is hardcoded to production quality for now.
const nvtt : : Quality QualityLevel = bPreview ? nvtt : : Quality_Fastest : nvtt : : Quality_Production ;
nvtt : : Format TextureFormat = nvtt : : Format_DXT1 ;
if ( PixelFormat = = PF_DXT1 )
{
TextureFormat = bSupportDXT1a ? nvtt : : Format_DXT1a : nvtt : : Format_DXT1 ;
}
else if ( PixelFormat = = PF_DXT3 )
{
TextureFormat = nvtt : : Format_DXT3 ;
}
else if ( PixelFormat = = PF_DXT5 & & bIsNormalMap )
{
TextureFormat = nvtt : : Format_DXT5n ;
}
else if ( PixelFormat = = PF_DXT5 )
{
TextureFormat = nvtt : : Format_DXT5 ;
}
else if ( PixelFormat = = PF_B8G8R8A8 )
{
TextureFormat = nvtt : : Format_RGBA ;
}
else if ( PixelFormat = = PF_BC4 )
{
TextureFormat = nvtt : : Format_BC4 ;
}
else if ( PixelFormat = = PF_BC5 )
{
TextureFormat = nvtt : : Format_BC5 ;
}
else
{
UE_LOG ( LogTextureFormatDXT , Fatal ,
TEXT ( " Unsupported EPixelFormat for compression: %u " ) ,
( uint32 ) PixelFormat
) ;
}
InputOptions . setTextureLayout ( nvtt : : TextureType_2D , SizeX , SizeY ) ;
// Not generating mips with NVTT, we will pass each mip in and compress it individually
InputOptions . setMipmapGeneration ( false , - 1 ) ;
verify ( InputOptions . setMipmapData ( SourceData , SizeX , SizeY ) ) ;
if ( bSRGB )
{
InputOptions . setGamma ( 2.2f , 2.2f ) ;
}
else
{
InputOptions . setGamma ( 1.0f , 1.0f ) ;
}
// Only used for mip and normal map generation
InputOptions . setWrapMode ( nvtt : : WrapMode_Mirror ) ;
InputOptions . setFormat ( nvtt : : InputFormat_BGRA_8UB ) ;
// Highest quality is 2x slower with only a small visual difference
// Might be worthwhile for normal maps though
CompressionOptions . setQuality ( QualityLevel ) ;
CompressionOptions . setFormat ( TextureFormat ) ;
if ( bIsNormalMap )
{
// For BC5 normal maps we don't care about the blue channel.
CompressionOptions . setColorWeights ( 1.0f , 1.0f , 0.0f ) ;
// Don't tell NVTT it's a normal map. It was producing noticeable artifacts during BC5 compression.
//InputOptions.setNormalMap(true);
}
else
{
CompressionOptions . setColorWeights ( 1 , 1 , 1 ) ;
}
Compressor . enableCudaAcceleration ( bUseCUDAAcceleration ) ;
//OutputHandler.ReserveMemory( Compressor.estimateSize(InputOptions, CompressionOptions) );
check ( OutputHandler . BufferEnd - OutputHandler . Buffer < = Compressor . estimateSize ( InputOptions , CompressionOptions ) ) ;
// We're not outputting a dds file so disable the header
OutputOptions . setOutputHeader ( false ) ;
OutputOptions . setOutputHandler ( & OutputHandler ) ;
OutputOptions . setErrorHandler ( & ErrorHandler ) ;
}
/** Run the compressor. */
bool Compress ( )
{
TRACE_CPUPROFILER_EVENT_SCOPE ( FNVTTCompressor : : Compress ) ;
return Compressor . process ( InputOptions , CompressionOptions , OutputOptions ) & & ErrorHandler . bSuccess ;
}
} ;
/**
* Asynchronous NVTT worker .
*/
class FAsyncNVTTWorker
{
public :
/**
* Initializes the data and creates the async compression task .
*/
FAsyncNVTTWorker ( FNVTTCompressor * InCompressor )
: Compressor ( InCompressor )
{
check ( Compressor ) ;
}
/** Compresses the texture. */
void DoWork ( )
{
bCompressionResults = Compressor - > Compress ( ) ;
}
/** Retrieve compression results. */
bool GetCompressionResults ( ) const { return bCompressionResults ; }
private :
/** The NVTT compressor. */
FNVTTCompressor * Compressor ;
/** true if compression was successful. */
bool bCompressionResults ;
} ;
namespace CompressionSettings
{
int32 BlocksPerBatch = 2048 ;
FAutoConsoleVariableRef BlocksPerBatch_CVar (
TEXT ( " Tex.AsyncDXTBlocksPerBatch " ) ,
BlocksPerBatch ,
TEXT ( " The number of blocks to compress in parallel for DXT compression. " )
) ;
}
/**
* Compresses an image using NVTT .
* @ param SourceData Source texture data to DXT compress , in BGRA 8 bit per channel unsigned format .
* @ param PixelFormat Texture format
* @ param SizeX Number of texels along the X - axis
* @ param SizeY Number of texels along the Y - axis
* @ param bSRGB Whether the texture is in SRGB space
* @ param bIsNormalMap Whether the texture is a normal map
* @ param OutCompressedData Compressed image data output by nvtt .
*/
static bool CompressImageUsingNVTT (
const void * SourceData ,
EPixelFormat PixelFormat ,
int32 SizeX ,
int32 SizeY ,
bool bSRGB ,
bool bIsNormalMap ,
bool bIsPreview ,
TArray64 < uint8 > & OutCompressedData
)
{
check ( PixelFormat = = PF_DXT1 | | PixelFormat = = PF_DXT3 | | PixelFormat = = PF_DXT5 | | PixelFormat = = PF_BC4 | | PixelFormat = = PF_BC5 ) ;
// Avoid dependency on GPixelFormats in RenderCore.
const int32 BlockSizeX = 4 ;
const int32 BlockSizeY = 4 ;
const int32 BlockBytes = ( PixelFormat = = PF_DXT1 | | PixelFormat = = PF_BC4 ) ? 8 : 16 ;
const int32 ImageBlocksX = FMath : : Max ( FMath : : DivideAndRoundUp ( SizeX , BlockSizeX ) , 1 ) ;
const int32 ImageBlocksY = FMath : : Max ( FMath : : DivideAndRoundUp ( SizeY , BlockSizeY ) , 1 ) ;
const int32 BlocksPerBatch = FMath : : Max < int32 > ( ImageBlocksX , FMath : : RoundUpToPowerOfTwo ( CompressionSettings : : BlocksPerBatch ) ) ;
const int32 RowsPerBatch = BlocksPerBatch / ImageBlocksX ;
const int32 NumBatches = ImageBlocksY / RowsPerBatch ;
// these round down, then if (RowsPerBatch * NumBatches) != ImageBlocksY , will encode without batches
// nvtt doesn't support 64-bit output sizes.
int64 OutDataSize = ( int64 ) ImageBlocksX * ImageBlocksY * BlockBytes ;
if ( OutDataSize > MAX_uint32 )
{
return false ;
}
// Allocate space to store compressed data.
OutCompressedData . Empty ( OutDataSize ) ;
OutCompressedData . AddUninitialized ( OutDataSize ) ;
if ( ImageBlocksX * ImageBlocksY < = BlocksPerBatch | |
BlocksPerBatch % ImageBlocksX ! = 0 | |
RowsPerBatch * NumBatches ! = ImageBlocksY )
{
FNVTTCompressor * Compressor = NULL ;
{
Compressor = new FNVTTCompressor (
SourceData ,
PixelFormat ,
SizeX ,
SizeY ,
bSRGB ,
bIsNormalMap ,
OutCompressedData . GetData ( ) ,
OutCompressedData . Num ( ) ,
bIsPreview
) ;
}
bool bSuccess = Compressor - > Compress ( ) ;
{
delete Compressor ;
Compressor = NULL ;
}
return bSuccess ;
}
int64 UncompressedStride = ( int64 ) RowsPerBatch * BlockSizeY * SizeX * sizeof ( FColor ) ;
int32 CompressedStride = RowsPerBatch * ImageBlocksX * BlockBytes ;
// Create compressors for each batch.
TIndirectArray < FNVTTCompressor > Compressors ;
Compressors . Empty ( NumBatches ) ;
{
const uint8 * Src = ( const uint8 * ) SourceData ;
uint8 * Dest = OutCompressedData . GetData ( ) ;
for ( int32 BatchIndex = 0 ; BatchIndex < NumBatches ; + + BatchIndex )
{
Compressors . Add ( new FNVTTCompressor (
Src ,
PixelFormat ,
SizeX ,
RowsPerBatch * BlockSizeY ,
bSRGB ,
bIsNormalMap ,
Dest ,
CompressedStride
) ) ;
Src + = UncompressedStride ;
Dest + = CompressedStride ;
}
}
// Asynchronously compress each batch.
bool bSuccess = true ;
{
TArray < FAsyncNVTTWorker > AsyncTasks ;
AsyncTasks . Reserve ( NumBatches ) ;
for ( int32 BatchIndex = 0 ; BatchIndex < NumBatches ; + + BatchIndex )
{
AsyncTasks . Emplace ( & Compressors [ BatchIndex ] ) ;
}
ParallelForTemplate ( AsyncTasks . Num ( ) , [ & AsyncTasks ] ( int32 TaskIndex )
{
AsyncTasks [ TaskIndex ] . DoWork ( ) ;
} , EParallelForFlags : : Unbalanced ) ;
for ( int32 BatchIndex = 0 ; BatchIndex < NumBatches ; + + BatchIndex )
{
bSuccess = bSuccess & & AsyncTasks [ BatchIndex ] . GetCompressionResults ( ) ;
}
}
// Release compressors
{
Compressors . Empty ( ) ;
}
return bSuccess ;
}
/**
* DXT texture format handler .
*/
class FTextureFormatDXT : public ITextureFormat
{
public :
virtual bool AllowParallelBuild ( ) const override
{
return true ;
}
virtual FName GetEncoderName ( FName Format ) const override
{
static const FName DXTName ( " EngineDXT " ) ;
return DXTName ;
}
virtual uint16 GetVersion (
FName Format ,
const struct FTextureBuildSettings * BuildSettings = nullptr
) const override
{
return 0 ;
}
virtual void GetSupportedFormats ( TArray < FName > & OutFormats ) const override
{
for ( int32 i = 0 ; i < UE_ARRAY_COUNT ( GSupportedTextureFormatNames ) ; + + i )
{
OutFormats . Add ( GSupportedTextureFormatNames [ i ] ) ;
}
}
virtual EPixelFormat GetEncodedPixelFormat ( const FTextureBuildSettings & BuildSettings , bool bImageHasAlphaChannel ) const override
{
if ( BuildSettings . TextureFormatName = = GTextureFormatNameDXT1 )
{
return PF_DXT1 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameDXT3 )
{
return PF_DXT3 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameDXT5 )
{
return PF_DXT5 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameAutoDXT )
{
return bImageHasAlphaChannel ? PF_DXT5 : PF_DXT1 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameDXT5n )
{
return PF_DXT5 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameBC5 )
{
return PF_BC5 ;
}
else if ( BuildSettings . TextureFormatName = = GTextureFormatNameBC4 )
{
return PF_BC4 ;
}
UE_LOG ( LogTextureFormatDXT , Fatal , TEXT ( " Unhandled texture format '%s' given to FTextureFormatDXT::GetEncodedPixelFormat() " ) , * BuildSettings . TextureFormatName . ToString ( ) ) ;
return PF_Unknown ;
}
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
{
TRACE_CPUPROFILER_EVENT_SCOPE ( FTextureFormatDXT : : CompressImage ) ;
// now we know NVTT will actually be used, Load the DLL :
const_cast < FTextureFormatDXT * > ( this ) - > LoadDLL ( ) ;
FImage Image ;
InImage . CopyTo ( Image , ERawImageFormat : : BGRA8 , BuildSettings . GetDestGammaSpace ( ) ) ;
EPixelFormat CompressedPixelFormat = GetEncodedPixelFormat ( BuildSettings , bImageHasAlphaChannel ) ;
bool bIsNormalMap = BuildSettings . TextureFormatName = = GTextureFormatNameDXT5n | | BuildSettings . TextureFormatName = = GTextureFormatNameBC5 ;
bool bCompressionSucceeded = true ;
int64 SliceSize = ( int64 ) Image . SizeX * Image . SizeY ;
if ( Image . NumSlices = = 1 & & OutCompressedImage . RawData . Num ( ) = = 0 )
{
// Avoid using a temp buffer when it's not needed
bCompressionSucceeded = CompressImageUsingNVTT (
( & Image . AsBGRA8 ( ) [ 0 ] ) ,
CompressedPixelFormat ,
Image . SizeX ,
Image . SizeY ,
Image . IsGammaCorrected ( ) ,
bIsNormalMap ,
false , // Daniel Lamb: Testing with this set to true didn't give large performance gain to lightmaps. Encoding of 140 lightmaps was 19.2seconds with preview 20.1 without preview. 11/30/2015
OutCompressedImage . RawData
) ;
}
else
{
for ( int32 SliceIndex = 0 ; SliceIndex < Image . NumSlices & & bCompressionSucceeded ; + + SliceIndex )
{
TArray64 < uint8 > CompressedSliceData ;
bCompressionSucceeded = CompressImageUsingNVTT (
( & Image . AsBGRA8 ( ) [ 0 ] ) + SliceIndex * SliceSize ,
CompressedPixelFormat ,
Image . SizeX ,
Image . SizeY ,
Image . IsGammaCorrected ( ) ,
bIsNormalMap ,
false , // Daniel Lamb: Testing with this set to true didn't give large performance gain to lightmaps. Encoding of 140 lightmaps was 19.2seconds with preview 20.1 without preview. 11/30/2015
CompressedSliceData
) ;
OutCompressedImage . RawData . Append ( MoveTemp ( CompressedSliceData ) ) ;
}
}
if ( bCompressionSucceeded )
{
// no more image size padding here
OutCompressedImage . SizeX = Image . SizeX ;
OutCompressedImage . SizeY = Image . SizeY ;
// old behavior :
//OutCompressedImage.SizeX = FMath::Max(Image.SizeX, 4);
//OutCompressedImage.SizeY = FMath::Max(Image.SizeY, 4);
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 ;
}
FTextureFormatDXT ( )
{
// don't LoadDLL until this format is actually used
}
void LoadDLL ( )
{
# if PLATFORM_WINDOWS
// nvtt_64.dll is set to DelayLoad by nvTextureTools.Build.cs
// manually load before any call to it, because it's not put in the binaries search path,
// and so we can get the AVX2 variant or not :
if ( nvTextureToolsHandle ! = nullptr )
{
return ;
}
// Lock so only one thread does init :
FScopeLock HandleLock ( & nvTextureToolsHandleLock ) ;
// double check inside lock :
if ( nvTextureToolsHandle ! = nullptr )
{
return ;
}
if ( FWindowsPlatformMisc : : HasAVX2InstructionSupport ( ) )
{
nvTextureToolsHandle = FPlatformProcess : : GetDllHandle ( * ( FPaths : : EngineDir ( ) / TEXT ( " Binaries/ThirdParty/nvTextureTools/Win64/AVX2/nvtt_64.dll " ) ) ) ;
}
else
{
nvTextureToolsHandle = FPlatformProcess : : GetDllHandle ( * ( FPaths : : EngineDir ( ) / TEXT ( " Binaries/ThirdParty/nvTextureTools/Win64/nvtt_64.dll " ) ) ) ;
}
# endif //PLATFORM_WINDOWS
}
~ FTextureFormatDXT ( )
{
# if PLATFORM_WINDOWS
if ( nvTextureToolsHandle ! = nullptr )
{
FPlatformProcess : : FreeDllHandle ( nvTextureToolsHandle ) ;
nvTextureToolsHandle = nullptr ;
}
# endif
}
# if PLATFORM_WINDOWS
// Handle to the nvtt dll
void * nvTextureToolsHandle = nullptr ;
FCriticalSection nvTextureToolsHandleLock ;
# endif //PLATFORM_WINDOWS
} ;
/**
* Module for DXT texture compression .
*/
static ITextureFormat * Singleton = NULL ;
class FTextureFormatDXTModule : public ITextureFormatModule
{
public :
virtual ~ FTextureFormatDXTModule ( )
{
delete Singleton ;
Singleton = NULL ;
}
virtual bool CanCallGetTextureFormats ( ) override { return false ; }
virtual ITextureFormat * GetTextureFormat ( )
{
if ( ! Singleton )
{
Singleton = new FTextureFormatDXT ( ) ;
}
return Singleton ;
}
// IModuleInterface implementation.
virtual void StartupModule ( ) override
{
}
virtual void ShutdownModule ( ) override
{
}
static inline UE : : DerivedData : : TBuildFunctionFactory < FDXTTextureBuildFunction > BuildFunctionFactory ;
private :
} ;
IMPLEMENT_MODULE ( FTextureFormatDXTModule , TextureFormatDXT ) ;