// Copyright Epic Games, Inc. All Rights Reserved. #include "Containers/SharedString.h" #include "ImageCore.h" #include "DDSFile.h" #include "Modules/ModuleManager.h" #include "TextureCompressorModule.h" #include "Interfaces/ITextureFormat.h" #include "Interfaces/ITextureFormatModule.h" #include "PixelFormat.h" #include "Engine/TextureDefines.h" #include "Misc/ConfigCacheIni.h" #include "Misc/ScopeLock.h" #include "Misc/SecureHash.h" #include "Async/ParallelFor.h" #include "Async/TaskGraphInterfaces.h" #include "Misc/FileHelper.h" #include "Runtime/Launch/Resources/Version.h" #include "Serialization/CompactBinary.h" #include "Serialization/CompactBinaryWriter.h" #include "DerivedDataBuildFunctionFactory.h" #include "Tasks/Task.h" #include "TextureBuildFunction.h" #include "HAL/FileManager.h" #include "HAL/LowLevelMemTracker.h" #include "Misc/WildcardString.h" #include "Misc/CommandLine.h" #include "oodle2tex.h" // Alternate job system - can set UseOodleExampleJobify in engine ini to enable. #include "Jobify/example_jobify.h" /********** Oodle Texture can do both RDO (rate distortion optimization) and non-RDO encoding to BC1-7. This is controlled using the project texture compression settings and the corresponding Compress Speed. The texture property Lossy Compression Amount is converted to an RDO Lambda to use. This property can be adjusted via LODGroup or per texture. If not set in either place, the project settings provide a default value. Oodle Texture can encode BC1-7. It does not currently encode ASTC or other mobile formats. ===================== TextureFormatOodle handles formats TFO_DXT1,etc. Use of this format (instead of DXT1) is enabled with TextureFormatPrefix in config, such as : \Engine\Config\BaseEngine.ini [AlternateTextureCompression] TextureCompressionFormat="TextureFormatOodle" TextureFormatPrefix="TFO_" When this is enabled, the formats like "DXT1" are renamed to "TFO_DXT1" and are handled by this encoder. Oodle Texture RDO encoding can be slow, but is cached in the DDC so should only be slow the first time. A fast local network shared DDC is recommended. RDO encoding and compression level can be enabled separately in the editor vs cooks using settings described below. ======================== Oodle Texture Settings ---------------------- TextureFormatOodle reads settings from Engine.ini ; they're created by default when not found. Note they are created in per-platform Engine.ini, you can find them and move them up to DefaultEngine if you want them to be global. The INI settings block looks like : [TextureFormatOodleSettings] bDebugColor=False GlobalLambdaMultiplier=1.0 The sense of the bools is set so that all-false is default behavior. bDebugColor : Fills the encoded texture with a solid color depending on their BCN format. This is a handy way to see that you are in fact getting Oodle Texture in your game. It's also an easy way to spot textures that aren't BCN compressed, since they will not be solid color. (for example I found that lots of the Unreal demo content uses "HDR" which is an uncompressed format, instead of "HDRCompressed" (BC6)) The color indicates the actual compressed format output (BC1-7). GlobalLambdaMultiplier : Takes all lambdas and scales them by this multiplier, so it affects the global default and the per-texture lambdas. It is recommended to leave this at 1.0 until you get near shipping your final game, at which point you could tweak it to 0.9 or 1.1 to adjust your package size without having to edit lots of per-texture lambdas. Oodle Texture lambda ---------------------- The "lambda" parameter is the most important way of controlling Oodle Texture RDO. "lambda" controls the tradeoff of size vs quality in the Rate Distortion Optimization. Finding the right lambda settings will be a collaboration between artists and programmers. Programmers and technical artists may wish to find a global lambda that meets your goals. Individual texture artists may wish to tweak the lambda per-texture when needed, but this should be rare - for the most part Oodle Texture quality is very predictable and good on most textures. Lambda first of all can be overridden per texture with the "LossyCompressionAmount" setting. This is a slider in the GUI in the editor that goes from Lowest to Highest. The default value is "Default" and we recommend leaving that there most of the time. If the per-texture LossyCompressionAmount is "Default", that means "inherit from LODGroup". The LODGroup gives you a logical group of textures where you can adjust the lambda on that whole set of textures rather than per-texture. For example here I have changed "World" LossyCompressionAmount to TLCA_High, and "WorldNormalMap" to TLCA_Low : [GlobalDefaults DeviceProfile] @TextureLODGroups=Group TextureLODGroups=(Group=TEXTUREGROUP_World,MinLODSize=1,MaxLODSize=8192,LODBias=0,MinMagFilter=aniso,MipFilter=point,MipGenSettings=TMGS_SimpleAverage,LossyCompressionAmount=TLCA_High) +TextureLODGroups=(Group=TEXTUREGROUP_WorldNormalMap,MinLODSize=1,MaxLODSize=8192,LODBias=0,MinMagFilter=aniso,MipFilter=point,MipGenSettings=TMGS_SimpleAverage,LossyCompressionAmount=TLCA_Low) +TextureLODGroups=(Group=TEXTUREGROUP_WorldSpecular,MinLODSize=1,MaxLODSize=8192,LODBias=0,MinMagFilter=aniso,MipFilter=point,MipGenSettings=TMGS_SimpleAverage) If the LossyCompressionAmount is not set on the LODGroup (which is the default), then it falls through to the global default, which is set in the texture compression project settings. At each stage, TLCA_Default means "inherit from parent". TLCA_None means disable RDO entirely. We do not recommend this, use TLCA_Lowest instead when you need very high quality. Note that the Unreal Editor texture dialog shows live compression results. When you're in the editor and you adjust the LossyCompressionAmount or import a new texture, it shows the Oodle Texture encoded result in the texture preview. *********/ DEFINE_LOG_CATEGORY_STATIC(LogTextureFormatOodle, Log, All); LLM_DEFINE_TAG(OodleTexture); /***************** * * Function pointer types for the Oodle Texture functions we need to import : * ********************/ OODEFFUNC typedef OodleTex_Err (OOEXPLINK t_fp_OodleTex_EncodeBCN_RDO_Ex)( OodleTex_BC to_bcn,void * to_bcn_blocks,OO_SINTa num_blocks, const OodleTex_Surface * from_surfaces,OO_SINTa num_from_surfaces,OodleTex_PixelFormat from_format, const OodleTex_Layout * layout, int rdo_lagrange_lambda, const OodleTex_RDO_Options * options, int num_job_threads,void * jobify_user_ptr); OODEFFUNC typedef OodleTex_Err(OOEXPLINK t_fp_OodleTex_DecodeBCN_LinearSurfaces)( OodleTex_Surface* to_surfaces, OO_SINTa num_to_surfaces, OodleTex_PixelFormat to_format, OodleTex_BC from_bcn, const void* from_bcn_blocks, OO_SINTa num_blocks, const OodleTex_Layout* layout); OODEFFUNC typedef void (OOEXPLINK t_fp_OodleTex_Plugins_SetAllocators)( t_fp_OodleTex_Plugin_MallocAligned * fp_OodleMallocAligned, t_fp_OodleTex_Plugin_Free * fp_OodleFree); OODEFFUNC typedef void (OOEXPLINK t_fp_OodleTex_Plugins_SetJobSystemAndCount)( t_fp_OodleTex_Plugin_RunJob * fp_RunJob, t_fp_OodleTex_Plugin_WaitJob * fp_WaitJob, int target_parallelism); OODEFFUNC typedef t_fp_OodleTex_Plugin_Printf * (OOEXPLINK t_fp_OodleTex_Plugins_SetPrintf)(t_fp_OodleTex_Plugin_Printf * fp_rrRawPrintf); OODEFFUNC typedef t_fp_OodleTex_Plugin_DisplayAssertion * (OOEXPLINK t_fp_OodleTex_Plugins_SetAssertion)(t_fp_OodleTex_Plugin_DisplayAssertion * fp_rrDisplayAssertion); OODEFFUNC typedef const char * (OOEXPLINK t_fp_OodleTex_Err_GetName)(OodleTex_Err error); OODEFFUNC typedef const char * (OOEXPLINK t_fp_OodleTex_PixelFormat_GetName)(OodleTex_PixelFormat pf); OODEFFUNC typedef const char * (OOEXPLINK t_fp_OodleTex_BC_GetName)(OodleTex_BC bcn); OODEFFUNC typedef const char * (OOEXPLINK t_fp_OodleTex_RDO_UniversalTiling_GetName)(OodleTex_RDO_UniversalTiling tiling); OODEFFUNC typedef OO_S32 (OOEXPLINK t_fp_OodleTex_BC_BytesPerBlock)(OodleTex_BC bcn); OODEFFUNC typedef OO_S32 (OOEXPLINK t_fp_OodleTex_PixelFormat_BytesPerPixel)(OodleTex_PixelFormat pf); OODEFFUNC typedef OodleTex_Err (OOEXPLINK t_fp_OodleTex_LogVersion)(void); /** * DebugInfo passed to the Jobify callbacks for tracing */ struct FOodleJobDebugInfo { FStringView DebugTexturePathName; int32 SizeX; int32 SizeY; OodleTex_BC OodleBCN; int RDOLambda; }; struct FOodleTextureVTable; static void TFO_Plugins_Init(); static void TFO_Plugins_Install(const FOodleTextureVTable * VTable); /** * * FOodleTextureVTable provides function calls to a specific version of the Oodle Texture dynamic lib * multiple FOodleTextureVTables may be loaded to support multi-version encoding * **/ struct FOodleTextureVTable { const TCHAR * VersionString = nullptr; FName Version; // LoadedDynamicLib is set on first use // if either of LoadedDynamicLib or LoadFailed is set, a load was attempted, don't try again // if both == 0, load has not been tried yet void * LoadedDynamicLib = nullptr; std::atomic LoadResult = 0; // 0 = not done, 1 = ok, -1 = fail FCriticalSection DynamicLibLoadLock; t_fp_OodleTex_EncodeBCN_RDO_Ex * fp_OodleTex_EncodeBCN_RDO_Ex = nullptr; t_fp_OodleTex_DecodeBCN_LinearSurfaces* fp_OodleTex_DecodeBCN_LinearSurfaces = nullptr; t_fp_OodleTex_Plugins_SetAllocators * fp_OodleTex_Plugins_SetAllocators = nullptr; t_fp_OodleTex_Plugins_SetJobSystemAndCount * fp_OodleTex_Plugins_SetJobSystemAndCount = nullptr; t_fp_OodleTex_Plugins_SetPrintf * fp_OodleTex_Plugins_SetPrintf = nullptr; t_fp_OodleTex_Plugins_SetAssertion * fp_OodleTex_Plugins_SetAssertion = nullptr; t_fp_OodleTex_Err_GetName * fp_OodleTex_Err_GetName = nullptr; t_fp_OodleTex_PixelFormat_GetName * fp_OodleTex_PixelFormat_GetName = nullptr; t_fp_OodleTex_BC_GetName * fp_OodleTex_BC_GetName = nullptr; t_fp_OodleTex_RDO_UniversalTiling_GetName * fp_OodleTex_RDO_UniversalTiling_GetName = nullptr; t_fp_OodleTex_BC_BytesPerBlock * fp_OodleTex_BC_BytesPerBlock = nullptr; t_fp_OodleTex_PixelFormat_BytesPerPixel * fp_OodleTex_PixelFormat_BytesPerPixel = nullptr; FOodleTextureVTable() { } void Init(const TCHAR * InVersionString) { // this runs from Module init, threads are not running VersionString = InVersionString; Version = FName(InVersionString); } bool TryLoad() { // load DLL on demand // this can run some threads so must be thread safe int GotLoadResult = LoadResult.load(std::memory_order_acquire); if ( GotLoadResult ) { return ( GotLoadResult > 0 ); } // else try to load : // Lock so only one thread does init : FScopeLock TryLoadLock(&DynamicLibLoadLock); // double check inside lock : GotLoadResult = LoadResult.load(std::memory_order_acquire); if ( GotLoadResult ) { return ( GotLoadResult > 0 ); } // TFO_DLL_PREFIX/SUFFIX is set by the build.cs with the right names for this platform FString DynamicLibName = FString(TFO_DLL_PREFIX) + VersionString + FString(TFO_DLL_SUFFIX); // I want to see this log by default in Cook+Editor , but not in TBW #ifndef VerboseIfNotEditor #if WITH_EDITOR #define VerboseIfNotEditor Display #else #define VerboseIfNotEditor Verbose #endif #endif UE_LOG(LogTextureFormatOodle,VerboseIfNotEditor,TEXT("Oodle Texture loading DLL: %s"), *DynamicLibName); void * DynamicLib = FPlatformProcess::GetDllHandle(*DynamicLibName); if ( DynamicLib == nullptr ) { UE_LOG(LogTextureFormatOodle, Warning, TEXT("Oodle Texture %s requested but could not be loaded"), *DynamicLibName); //don't change Version after Init, not thread safe (FName is not atomic) //Version = FName("invalid"); // so we can't be found in later searches LoadResult.store(-1,std::memory_order_release); // publish return false; } fp_OodleTex_Err_GetName = (t_fp_OodleTex_Err_GetName *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_Err_GetName") ); check( fp_OodleTex_Err_GetName != nullptr ); fp_OodleTex_Plugins_SetPrintf = (t_fp_OodleTex_Plugins_SetPrintf *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_Plugins_SetPrintf") ); check( fp_OodleTex_Plugins_SetPrintf != nullptr ); t_fp_OodleTex_LogVersion * fp_OodleTex_LogVersion = (t_fp_OodleTex_LogVersion *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_LogVersion") ); check( fp_OodleTex_LogVersion != nullptr ); // make Printf go to NULL for LogVersion : (*fp_OodleTex_Plugins_SetPrintf)(nullptr); // Get LogVersion so we can get an error code to check the DLL is okay : OodleTex_Err OodleErr = (*fp_OodleTex_LogVersion)(); if ( OodleErr != OodleTex_Err_OK ) { const char * OodleErrStr = (*fp_OodleTex_Err_GetName)(OodleErr); UE_LOG(LogTextureFormatOodle, Warning, TEXT("Oodle Texture %s loaded but failed in LogVersion with error %d=%s"), *DynamicLibName, (int)OodleErr, ANSI_TO_TCHAR(OodleErrStr) ); //don't change Version after Init, not thread safe (FName is not atomic) //Version = FName("invalid"); // so we can't be found in later searches LoadResult.store(-1,std::memory_order_release); // publish return false; } fp_OodleTex_EncodeBCN_RDO_Ex = (t_fp_OodleTex_EncodeBCN_RDO_Ex *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_EncodeBCN_RDO_Ex") ); check( fp_OodleTex_EncodeBCN_RDO_Ex != nullptr ); fp_OodleTex_DecodeBCN_LinearSurfaces = (t_fp_OodleTex_DecodeBCN_LinearSurfaces*)FPlatformProcess::GetDllExport(DynamicLib, TEXT("OodleTex_DecodeBCN_LinearSurfaces")); check(fp_OodleTex_DecodeBCN_LinearSurfaces != nullptr); fp_OodleTex_Plugins_SetAllocators = (t_fp_OodleTex_Plugins_SetAllocators *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_Plugins_SetAllocators") ); check( fp_OodleTex_Plugins_SetAllocators != nullptr ); fp_OodleTex_Plugins_SetJobSystemAndCount = (t_fp_OodleTex_Plugins_SetJobSystemAndCount *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_Plugins_SetJobSystemAndCount") ); check( fp_OodleTex_Plugins_SetJobSystemAndCount != nullptr ); fp_OodleTex_Plugins_SetAssertion = (t_fp_OodleTex_Plugins_SetAssertion *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_Plugins_SetAssertion") ); check( fp_OodleTex_Plugins_SetAssertion != nullptr ); fp_OodleTex_PixelFormat_GetName = (t_fp_OodleTex_PixelFormat_GetName *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_PixelFormat_GetName") ); check( fp_OodleTex_PixelFormat_GetName != nullptr ); fp_OodleTex_BC_GetName = (t_fp_OodleTex_BC_GetName *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_BC_GetName") ); check( fp_OodleTex_BC_GetName != nullptr ); fp_OodleTex_RDO_UniversalTiling_GetName = (t_fp_OodleTex_RDO_UniversalTiling_GetName *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_RDO_UniversalTiling_GetName") ); check( fp_OodleTex_RDO_UniversalTiling_GetName != nullptr ); fp_OodleTex_BC_BytesPerBlock = (t_fp_OodleTex_BC_BytesPerBlock *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_BC_BytesPerBlock") ); check( fp_OodleTex_BC_BytesPerBlock != nullptr ); fp_OodleTex_PixelFormat_BytesPerPixel = (t_fp_OodleTex_PixelFormat_BytesPerPixel *) FPlatformProcess::GetDllExport( DynamicLib, TEXT("OodleTex_PixelFormat_BytesPerPixel") ); check( fp_OodleTex_PixelFormat_BytesPerPixel != nullptr ); TFO_Plugins_Install(this); LoadedDynamicLib = DynamicLib; LoadResult.store(1,std::memory_order_release); // publish return true; } ~FOodleTextureVTable() { if ( LoadedDynamicLib ) { FPlatformProcess::FreeDllHandle(LoadedDynamicLib); LoadedDynamicLib = nullptr; } } }; class FOodleTextureBuildFunction final : public FTextureBuildFunction { const UE::FUtf8SharedString& GetName() const final { static const UE::FUtf8SharedString Name(UTF8TEXTVIEW("OodleTexture")); return Name; } void GetVersion(UE::DerivedData::FBuildVersionBuilder& Builder, ITextureFormat*& OutTextureFormatVersioning) const final { static FGuid Version(TEXT("e6b8884f-923a-44a1-8da1-298fb48865b2")); Builder << Version; OutTextureFormatVersioning = FModuleManager::GetModuleChecked(TEXT("TextureFormatOodle")).GetTextureFormat(); } }; struct FOodlePixelFormatMapping { UE::DDS::EDXGIFormat DXGIFormat; OodleTex_PixelFormat OodlePF; bool bHasAlpha; }; // mapping from/to UNORM formats; sRGB-ness is handled separately. // when there are multiple dxgi formats mapping to the same Oodle format, the first one is used // for conversions from Oodle to DXGI static FOodlePixelFormatMapping PixelFormatMap[] = { // dxgi ootex has_alpha { UE::DDS::EDXGIFormat::R32G32B32A32_FLOAT, OodleTex_PixelFormat_4_F32_RGBA, true }, { UE::DDS::EDXGIFormat::R32G32B32_FLOAT, OodleTex_PixelFormat_3_F32_RGB, true }, { UE::DDS::EDXGIFormat::R16G16B16A16_FLOAT, OodleTex_PixelFormat_4_F16_RGBA, true }, { UE::DDS::EDXGIFormat::R8G8B8A8_UNORM, OodleTex_PixelFormat_4_U8_RGBA, true }, { UE::DDS::EDXGIFormat::R16G16B16A16_UNORM, OodleTex_PixelFormat_4_U16, true }, { UE::DDS::EDXGIFormat::R16G16_UNORM, OodleTex_PixelFormat_2_U16, false }, { UE::DDS::EDXGIFormat::R16G16_SNORM, OodleTex_PixelFormat_2_S16, false }, { UE::DDS::EDXGIFormat::R8G8_UNORM, OodleTex_PixelFormat_2_U8, false }, { UE::DDS::EDXGIFormat::R8G8_SNORM, OodleTex_PixelFormat_2_S8, false }, { UE::DDS::EDXGIFormat::R16_UNORM, OodleTex_PixelFormat_1_U16, false }, { UE::DDS::EDXGIFormat::R16_SNORM, OodleTex_PixelFormat_1_S16, false }, { UE::DDS::EDXGIFormat::R8_UNORM, OodleTex_PixelFormat_1_U8, false }, { UE::DDS::EDXGIFormat::R8_SNORM, OodleTex_PixelFormat_1_S8, false }, { UE::DDS::EDXGIFormat::B8G8R8A8_UNORM, OodleTex_PixelFormat_4_U8_BGRA, true }, { UE::DDS::EDXGIFormat::B8G8R8X8_UNORM, OodleTex_PixelFormat_4_U8_BGRx, false }, }; static OodleTex_PixelFormat OodlePFFromDXGIFormat(UE::DDS::EDXGIFormat InFormat) { InFormat = UE::DDS::DXGIFormatRemoveSRGB(InFormat); for (size_t i = 0; i < sizeof(PixelFormatMap) / sizeof(*PixelFormatMap); ++i) { if (PixelFormatMap[i].DXGIFormat == InFormat) { return PixelFormatMap[i].OodlePF; } } return OodleTex_PixelFormat_Invalid; } // don't need this for all DXGI formats, just the ones we can translate to Oodle Texture formats static bool DXGIFormatHasAlpha(UE::DDS::EDXGIFormat InFormat) { InFormat = UE::DDS::DXGIFormatRemoveSRGB(InFormat); for (size_t i = 0; i < sizeof(PixelFormatMap) / sizeof(*PixelFormatMap); ++i) { if (PixelFormatMap[i].DXGIFormat == InFormat) { return PixelFormatMap[i].bHasAlpha; } } // when we don't know the format, the answer doesn't really matter; let's just say "yes" return true; } static UE::DDS::EDXGIFormat DXGIFormatFromOodlePF(OodleTex_PixelFormat pf) { for (size_t i = 0; i < sizeof(PixelFormatMap) / sizeof(*PixelFormatMap); ++i) { if (PixelFormatMap[i].OodlePF == pf) { return PixelFormatMap[i].DXGIFormat; } } return UE::DDS::EDXGIFormat::UNKNOWN; } struct FOodleBCMapping { UE::DDS::EDXGIFormat DXGIFormat; OodleTex_BC OodleBC; }; static FOodleBCMapping BCFormatMap[] = { { UE::DDS::EDXGIFormat::BC1_UNORM, OodleTex_BC1 }, { UE::DDS::EDXGIFormat::BC1_UNORM, OodleTex_BC1_WithTransparency }, { UE::DDS::EDXGIFormat::BC2_UNORM, OodleTex_BC2 }, { UE::DDS::EDXGIFormat::BC3_UNORM, OodleTex_BC3 }, { UE::DDS::EDXGIFormat::BC4_UNORM, OodleTex_BC4U }, { UE::DDS::EDXGIFormat::BC4_SNORM, OodleTex_BC4S }, { UE::DDS::EDXGIFormat::BC5_UNORM, OodleTex_BC5U }, { UE::DDS::EDXGIFormat::BC5_SNORM, OodleTex_BC5S }, { UE::DDS::EDXGIFormat::BC6H_UF16, OodleTex_BC6U }, { UE::DDS::EDXGIFormat::BC6H_SF16, OodleTex_BC6S }, { UE::DDS::EDXGIFormat::BC7_UNORM, OodleTex_BC7RGBA }, { UE::DDS::EDXGIFormat::BC7_UNORM, OodleTex_BC7RGB }, }; static OodleTex_BC OodleBCFromDXGIFormat(UE::DDS::EDXGIFormat InFormat) { InFormat = UE::DDS::DXGIFormatRemoveSRGB(InFormat); for (size_t i = 0; i < sizeof(BCFormatMap) / sizeof(*BCFormatMap); ++i) { if (BCFormatMap[i].DXGIFormat == InFormat) { return BCFormatMap[i].OodleBC; } } return OodleTex_BC_Invalid; } static UE::DDS::EDXGIFormat DXGIFormatFromOodleBC(OodleTex_BC InBC) { for (size_t i = 0; i < sizeof(BCFormatMap) / sizeof(*BCFormatMap); ++i) { if (BCFormatMap[i].OodleBC == InBC) { return BCFormatMap[i].DXGIFormat; } } return UE::DDS::EDXGIFormat::UNKNOWN; } // user data passed to Oodle Jobify system static int OodleJobifyNumThreads = 0; static void *OodleJobifyUserPointer = nullptr; static bool OodleJobifyUseExampleJobify = false; // enable this to make the DDC key unique (per build) for testing //#define DO_FORCE_UNIQUE_DDC_KEY_PER_BUILD #define ENUSUPPORTED_FORMATS(op) \ op(DXT1) \ op(DXT3) \ op(DXT5) \ op(DXT5n) \ op(AutoDXT) \ op(BC4) \ op(BC5) \ op(BC6H) \ op(BC7) // register support for TFO_ prefixed names like "TFO_DXT1" #define TEXTURE_FORMAT_PREFIX "TFO_" #define DECL_FORMAT_NAME(FormatName) static FName GTextureFormatName##FormatName = FName(TEXT(TEXTURE_FORMAT_PREFIX #FormatName)); ENUSUPPORTED_FORMATS(DECL_FORMAT_NAME); #undef DECL_FORMAT_NAME #define DECL_FORMAT_NAME_ENTRY(FormatName) GTextureFormatName##FormatName , static FName GSupportedTextureFormatNames[] = { ENUSUPPORTED_FORMATS(DECL_FORMAT_NAME_ENTRY) }; #undef DECL_FORMAT_NAME_ENTRY #undef ENUSUPPORTED_FORMATS class FTextureFormatOodleConfig { public: struct FLocalDebugConfig { FLocalDebugConfig() : LogVerbosity(0) { } FString DebugDumpFilter; // dump textures that were encoded int LogVerbosity; // 0-2 ; 0=never, 1=large only, 2=always }; FTextureFormatOodleConfig() : bDebugColor(false), GlobalLambdaMultiplier(1.f) { } const FLocalDebugConfig& GetLocalDebugConfig() const { return LocalDebugConfig; } void ImportFromConfigCache() { const TCHAR* IniSection = TEXT("TextureFormatOodleSettings"); // // Note that while this gets called during singleton init for the module, // the INIs don't exist when we're being run as a texture build worker, // so all of these GConfig calls do nothing. // // Class config variables GConfig->GetBool(IniSection, TEXT("bDebugColor"), bDebugColor, GEngineIni); GConfig->GetString(IniSection, TEXT("DebugDumpFilter"), LocalDebugConfig.DebugDumpFilter, GEngineIni); GConfig->GetInt(IniSection, TEXT("LogVerbosity"), LocalDebugConfig.LogVerbosity, GEngineIni); GConfig->GetFloat(IniSection, TEXT("GlobalLambdaMultiplier"), GlobalLambdaMultiplier, GEngineIni); FString CmdLineString; if (FParse::Value(FCommandLine::Get(), TEXT("-OodleDebugDumpFilter="), CmdLineString) ) { UE_LOG(LogTextureFormatOodle, Display, TEXT("Enabling debug dump from command line: -OodleDebugDumpFilter=%s"), *CmdLineString); LocalDebugConfig.DebugDumpFilter = CmdLineString; } if (FParse::Param(FCommandLine::Get(), TEXT("OodleDebugColor"))) { UE_LOG(LogTextureFormatOodle, Display, TEXT("Enabling debug color encoding from command line (-OodleDebugColor)")); bDebugColor = true; } // sanitize config values : if ( GlobalLambdaMultiplier <= 0.f ) { GlobalLambdaMultiplier = 1.f; } UE_LOG(LogTextureFormatOodle,VerboseIfNotEditor, TEXT("Oodle Texture TFO init; latest sdk version = %s"),TEXT(OodleTextureVersion) ); #ifdef DO_FORCE_UNIQUE_DDC_KEY_PER_BUILD UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle Texture DO_FORCE_UNIQUE_DDC_KEY_PER_BUILD")); #endif } FCbObject ExportToCb(const FTextureBuildSettings& BuildSettings) const { // // Here we write config stuff to the packet that gets sent to the build // workers. // // This is only for stuff that isn't already part of the build settings. // FCbWriter Writer; Writer.BeginObject("TextureFormatOodleSettings"); if (bDebugColor) { Writer.AddBool("bDebugColor", bDebugColor); } if (GlobalLambdaMultiplier != 1.f) { Writer.AddFloat("GlobalLambdaMultipler", GlobalLambdaMultiplier); } Writer.EndObject(); return Writer.Save().AsObject(); } void GetOodleCompressParameters(EPixelFormat * OutCompressedPixelFormat,int * OutRDOLambda, OodleTex_EncodeEffortLevel * OutEffortLevel, bool * bOutDebugColor, OodleTex_RDO_UniversalTiling* OutRDOUniversalTiling, OodleTex_BCNFlags* OutBCNFlags, const struct FTextureBuildSettings& InBuildSettings, bool bHasAlpha) const { //TRACE_CPUPROFILER_EVENT_SCOPE(Texture.GetOodleCompressParameters); FName TextureFormatName = InBuildSettings.BaseTextureFormatName; EPixelFormat CompressedPixelFormat = PF_Unknown; if (TextureFormatName == GTextureFormatNameDXT1) { CompressedPixelFormat = PF_DXT1; } else if (TextureFormatName == GTextureFormatNameDXT3) { CompressedPixelFormat = PF_DXT3; } else if (TextureFormatName == GTextureFormatNameDXT5) { CompressedPixelFormat = PF_DXT5; } else if (TextureFormatName == GTextureFormatNameAutoDXT) { //not all "AutoDXT" comes in here // some AutoDXT is converted to "DXT1" before it gets here // (by GetDefaultTextureFormatName if "compress no alpha" is set) CompressedPixelFormat = bHasAlpha ? PF_DXT5 : PF_DXT1; } else if (TextureFormatName == GTextureFormatNameDXT5n) { // Unreal already has global UseDXT5NormalMap config option // EngineSettings.GetString(TEXT("SystemSettings"), TEXT("Compat.UseDXT5NormalMaps") // if that is false (which is the default) they use BC5 // so this should be rarely use // (we prefer BC5 over DXT5n) CompressedPixelFormat = PF_DXT5; } else if (TextureFormatName == GTextureFormatNameBC4) { CompressedPixelFormat = PF_BC4; } else if (TextureFormatName == GTextureFormatNameBC5) { CompressedPixelFormat = PF_BC5; } else if (TextureFormatName == GTextureFormatNameBC6H) { CompressedPixelFormat = PF_BC6H; } else if (TextureFormatName == GTextureFormatNameBC7) { CompressedPixelFormat = PF_BC7; } else { UE_LOG(LogTextureFormatOodle,Fatal, TEXT("Unsupported TextureFormatName for compression: %s"), *TextureFormatName.ToString() ); } *OutCompressedPixelFormat = CompressedPixelFormat; // Use the DDC2 provided value if it exists. bool bUseDebugColor = InBuildSettings.FormatConfigOverride.FindView("bDebugColor").AsBool(bDebugColor); float UseGlobalLambdaMultiplier = InBuildSettings.FormatConfigOverride.FindView("GlobalLambdaMultipler").AsFloat(GlobalLambdaMultiplier); // // Convert general build settings in to oodle relevant values. // int RDOLambda = InBuildSettings.OodleRDO; if (RDOLambda > 0 && UseGlobalLambdaMultiplier != 1.f) { RDOLambda = (int)(UseGlobalLambdaMultiplier * RDOLambda + 0.5f ); // don't let it change to 0 : if ( RDOLambda <= 0 ) { RDOLambda = 1; } } RDOLambda = FMath::Clamp(RDOLambda,0,100); // EffortLevel might be set to faster modes for previewing vs cooking or something // but I don't see people setting that per-Texture or in lod groups or any of that // it's more about cook mode (fast vs final bake) // Note InBuildSettings.OodleEncodeEffort is an ETextureEncodeEffort // we cast directly to OodleTex_EncodeEffortLevel // the enum values must match exactly OodleTex_EncodeEffortLevel EffortLevel = (OodleTex_EncodeEffortLevel)InBuildSettings.OodleEncodeEffort; if (EffortLevel != OodleTex_EncodeEffortLevel_Default && EffortLevel != OodleTex_EncodeEffortLevel_Low && EffortLevel != OodleTex_EncodeEffortLevel_Normal && EffortLevel != OodleTex_EncodeEffortLevel_High) { UE_LOG(LogTextureFormatOodle, Warning, TEXT("Invalid effort level passed to texture format oodle: %d is invalid, using default"), (uint32)EffortLevel); EffortLevel = OodleTex_EncodeEffortLevel_Default; } // map Unreal ETextureUniversalTiling to OodleTex_RDO_UniversalTiling // enum values must match exactly OodleTex_RDO_UniversalTiling UniversalTiling = (OodleTex_RDO_UniversalTiling)InBuildSettings.OodleUniversalTiling; if ( UniversalTiling != OodleTex_RDO_UniversalTiling_Disable && UniversalTiling != OodleTex_RDO_UniversalTiling_256KB && UniversalTiling != OodleTex_RDO_UniversalTiling_64KB ) { UE_LOG(LogTextureFormatOodle, Warning, TEXT("Invalid universal tiling value passed to texture format oodle: %d is invalid, disabling"), (uint32)UniversalTiling); UniversalTiling = OodleTex_RDO_UniversalTiling_Disable; } OodleTex_BCNFlags BCNFlags = OodleTex_BCNFlags_None; if (InBuildSettings.bOodlePreserveExtremes) { BCNFlags = (OodleTex_BCNFlags)((uint32)BCNFlags | (uint32)OodleTex_BCNFlag_PreserveExtremes_BC3457); } if (RDOLambda == 0) { // Universal tiling doesn't make sense without RDO. UniversalTiling = OodleTex_RDO_UniversalTiling_Disable; } #if 0 // leave this if 0 block for developers to toggle for debugging // Debug Color any non-RDO // easy way to make sure you're seeing RDO textures if (RDOLambda == 0) { bUseDebugColor = true; } #endif *bOutDebugColor = bUseDebugColor; *OutRDOLambda = RDOLambda; *OutEffortLevel = EffortLevel; *OutRDOUniversalTiling = UniversalTiling; *OutBCNFlags = BCNFlags; } private: // the sense of these bools is set so that default behavior = all false bool bDebugColor; // color textures by their BCN, for data discovery // after lambda is set, multiply by this scale factor : // (multiplies the default and per-Texture overrides) // is intended to let you do last minute whole-game adjustment float GlobalLambdaMultiplier; FLocalDebugConfig LocalDebugConfig; }; class FTextureFormatOodle : public ITextureFormat { public: FTextureFormatOodleConfig GlobalFormatConfig; TArray VTables; FName OodleTextureVersionLatest; FName OodleTextureSdkVersionToUseIfNone; FTextureFormatOodle() : OodleTextureVersionLatest(OodleTextureVersion), OodleTextureSdkVersionToUseIfNone("2.9.5") { // OodleTextureSdkVersionToUseIfNone is the fallback version to use if none is in the Texture uasset // and also no remap pref is set // it should not be latest; it should be oldest (2.9.5) // OodleTextureSdkVersionToUseIfNone should never be changed // if you want to map none to a newer version use config ini option AlternateTextureCompression/OodleTextureSdkVersionToUseIfNone } static FGuid GetDecodeBuildFunctionVersionGuid() { static FGuid Version(TEXT("52C604A9-F0D5-4108-8F76-AFF78C2BC039")); return Version; } static FUtf8StringView GetDecodeBuildFunctionNameStatic() { return UTF8TEXTVIEW("FDecodeTextureFormatOodle"); } virtual const FUtf8StringView GetDecodeBuildFunctionName() const override final { return GetDecodeBuildFunctionNameStatic(); } virtual ~FTextureFormatOodle() { } virtual bool AllowParallelBuild() const override { return true; } virtual bool SupportsEncodeSpeed(FName Format, const ITargetPlatformSettings* TargetPlatform) const override { return true; } virtual FName GetEncoderName(FName Format) const override { static const FName OodleName("UE5 Oodle Texture"); return OodleName; } virtual FCbObject ExportGlobalFormatConfig(const FTextureBuildSettings& BuildSettings) const override { return GlobalFormatConfig.ExportToCb(BuildSettings); } bool Init() { TFO_Plugins_Init(); // this is done at Singleton init time, the first time GetTextureFormat() is called GlobalFormatConfig.ImportFromConfigCache(); // load ALL Oodle DLL versions we support : // !! add new versions of Oodle here !! const TCHAR * OodleTextureVersions[] = { TEXT("2.9.5"), TEXT("2.9.6"), TEXT("2.9.7"), TEXT("2.9.8"), TEXT("2.9.9"), TEXT("2.9.10"), TEXT("2.9.11"), TEXT("2.9.12"), TEXT("2.9.13"), TEXT("2.9.14"), }; const int32 OodleTextureVersionsCount = (int32)( sizeof(OodleTextureVersions)/sizeof(OodleTextureVersions[0]) ); VTables.SetNum(OodleTextureVersionsCount); // set up the VTable versions but don't actually load them yet // they will be loaded on first use for(int32 i=0;i(VTables[i]).TryLoad() ) return nullptr; return &(VTables[i]); } } return nullptr; } // increment this to invalidate Derived Data Cache to recompress everything // 16->17 incremented due to incorrect routing of CanAcceptNonF32Source. We are the only thing it's true for. #define DDC_OODLE_TEXTURE_VERSION 17 virtual uint16 GetVersion(FName Format, const FTextureBuildSettings* InBuildSettings) const override { // note: InBuildSettings == NULL is used by GetVersionFormatNumbersForIniVersionStrings // just to get a displayable version number return DDC_OODLE_TEXTURE_VERSION; } virtual FString GetAlternateTextureFormatPrefix() const override { static const FString Prefix(TEXT(TEXTURE_FORMAT_PREFIX)); return Prefix; } virtual FName GetLatestSdkVersion() const override { return OodleTextureVersionLatest; } virtual FString GetDerivedDataKeyString(const FTextureBuildSettings& InBuildSettings, int32 InMipCount, const FIntVector3& InMip0Dimensions) const override { // return all parameters that affect our output Texture // so if any of them change, we rebuild int RDOLambda; OodleTex_EncodeEffortLevel EffortLevel; OodleTex_RDO_UniversalTiling RDOUniversalTiling; OodleTex_BCNFlags BCNFlags; EPixelFormat CompressedPixelFormat; bool bDebugColor; // @todo Oodle : use InBuildSettings.GetOutputAlphaFromKnownAlphaOrFallback() instead (but that could change DDC keys) bool bHasAlpha = !InBuildSettings.bForceNoAlphaChannel; GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat, &RDOLambda, &EffortLevel, &bDebugColor, &RDOUniversalTiling, &BCNFlags, InBuildSettings, bHasAlpha); int icpf = (int)CompressedPixelFormat; check(RDOLambda<256); if (bDebugColor) { // Debug Color is solid or check for RDO/ no RDO // so make different DDC keys : if ( RDOLambda == 0 ) RDOLambda = 256; else RDOLambda = 257; EffortLevel = OodleTex_EncodeEffortLevel_Default; } FString DDCString = FString::Printf(TEXT("Oodle_CPF%d_L%d_E%d"), icpf, (int)RDOLambda, (int)EffortLevel); if (RDOUniversalTiling != OodleTex_RDO_UniversalTiling_Disable) { DDCString += FString::Printf(TEXT("_UT%d"), (int)RDOUniversalTiling); } if (BCNFlags != OodleTex_BCNFlags_None) { DDCString += FString::Printf(TEXT("_BCNF%ud"), (uint32)BCNFlags); } // OodleTextureSdkVersion was added ; keys where OodleTextureSdkVersion is none are unchanged if ( ! InBuildSettings.OodleTextureSdkVersion.IsNone() ) { DDCString += TEXT("_V"); // concatenate VersionString without . characters which are illegal in DDC // version is something like "2.9.5" , we'll add something like "_V295" FName UseOodleTextureSdkVersion = ValidateOodleTextureSdkVersion(InBuildSettings.OodleTextureSdkVersion); FString VersionString = UseOodleTextureSdkVersion.ToString(); for(int32 i=0;i SeenBadVersion; bool bAlreadySeen = false; BadVersionLock.Lock(); SeenBadVersion.Add(DesiredOodleTextureSdkVersion, &bAlreadySeen); BadVersionLock.Unlock(); if (!bAlreadySeen) { UE_LOG(LogTextureFormatOodle,Display, TEXT("Unsupported OodleTextureSdkVersion: %s ; instead using: %s"), *DesiredOodleTextureSdkVersion.ToString(), *OodleTextureVersionLatest.ToString() ); } return OodleTextureVersionLatest; } else { return DesiredOodleTextureSdkVersion; } } virtual void GetSupportedFormats(TArray& OutFormats) const override { OutFormats.Append(GSupportedTextureFormatNames, sizeof(GSupportedTextureFormatNames)/sizeof(GSupportedTextureFormatNames[0]) ); } virtual EPixelFormat GetEncodedPixelFormat(const FTextureBuildSettings& InBuildSettings, bool bImageHasAlphaChannel) const { int RDOLambda; OodleTex_EncodeEffortLevel EffortLevel; OodleTex_RDO_UniversalTiling RDOUniversalTiling; OodleTex_BCNFlags BCNFlags; EPixelFormat CompressedPixelFormat; bool bDebugColor; GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat, &RDOLambda, &EffortLevel, &bDebugColor, &RDOUniversalTiling, &BCNFlags, InBuildSettings, bImageHasAlphaChannel); return CompressedPixelFormat; } static void DebugDumpDDS(const FStringView & DebugTexturePathName, int32 SizeX,int32 SizeY,int32 Slice, UE::DDS::EDXGIFormat DebugFormat, const TCHAR * InOrOut, const void * PixelData, size_t PixelDataSize) { if (DebugFormat != UE::DDS::EDXGIFormat::UNKNOWN) { UE::DDS::FDDSFile* DDS = UE::DDS::FDDSFile::CreateEmpty2D(SizeX, SizeY, 1, DebugFormat, UE::DDS::FDDSFile::CREATE_FLAG_NONE); if ( DDS->Mips[0].DataSize != PixelDataSize ) { UE_LOG(LogTextureFormatOodle, Warning, TEXT("DebugDump mip sizes don't match %dx%d: %lld != %lld"), SizeX,SizeY, DDS->Mips[0].DataSize, PixelDataSize); } size_t MipCopySize = FMath::Min(PixelDataSize,(size_t)DDS->Mips[0].DataSize); FMemory::Memcpy(DDS->Mips[0].Data, PixelData, MipCopySize); const TCHAR * FormatStr = DXGIFormatGetName(DebugFormat); FString FileName = FString::Printf(TEXT("%.*s_%s_%dx%d_S%d_%s.dds"), DebugTexturePathName.Len(), DebugTexturePathName.GetData(), FormatStr, SizeX, SizeY, Slice, InOrOut); // Object paths a) can contain slashes as its a path, and we dont want a hierarchy and b) can have random characters we don't want FileName = FPaths::MakeValidFileName(FileName, TEXT('_')); // limit file name len // full path will still likely be longer than _MAX_PATH which breaks many programs if ( FileName.Len() >= 256 ) { FileName = FileName.Right(255); } FileName = FPaths::ProjectSavedDir() + TEXT("OodleDebugImages/") + FileName; FArchive* Ar = IFileManager::Get().CreateFileWriter(*FileName); if (Ar != nullptr) { TArray64 DdsBytes; if (DDS->WriteDDS(DdsBytes) == UE::DDS::EDDSError::OK) { Ar->Serialize(DdsBytes.GetData(), DdsBytes.Num()); } Ar->Close(); delete Ar; } else { UE_LOG(LogTextureFormatOodle, Error, TEXT("Failed to open DDS debug file: %s"), *FileName); } delete DDS; } } bool CanAcceptNonF32Source(FName Format) const override { return true; } // Returns OodleTex_BC_Invalid if unsupported. static OodleTex_BC OodleBCNFromPixelFormat(EPixelFormat InPixelFormat) { switch (InPixelFormat) { case PF_DXT1: return OodleTex_BC1_WithTransparency; case PF_DXT3: return OodleTex_BC2; case PF_DXT5: return OodleTex_BC3; case PF_BC4: return OodleTex_BC4U; case PF_BC5: return OodleTex_BC5U; case PF_BC6H: return OodleTex_BC6U; case PF_BC7: return OodleTex_BC7RGBA; default: return OodleTex_BC_Invalid; } } virtual bool CanDecodeFormat(EPixelFormat InPixelFormat) const { return OodleBCNFromPixelFormat(InPixelFormat) != OodleTex_BC_Invalid; } virtual bool DecodeImage(int32 InSizeX, int32 InSizeY, int32 InNumSlices, EPixelFormat InPixelFormat, bool bInSRGB, const FName& InTextureFormatName, FSharedBuffer InEncodedData, FImage& OutImage, FStringView InTextureName) const { // Should we go to linear or not? OodleTex_PixelFormat DestOoFormat = OodleTex_PixelFormat_4_U8_BGRA; ERawImageFormat::Type DestFormat = ERawImageFormat::BGRA8; if (InPixelFormat == PF_BC6H) { DestFormat = ERawImageFormat::RGBA16F; DestOoFormat = OodleTex_PixelFormat_4_F16_RGBA; } OodleTex_BC OodleBCN = OodleBCNFromPixelFormat(InPixelFormat); if (OodleBCN == OodleTex_BC_Invalid) { return false; } OutImage.Init(InSizeX, InSizeY, InNumSlices, DestFormat, EGammaSpace::Linear); uint64 BlocksPerSlice = Align(InSizeX, 4) * Align(InSizeY, 4) / 16; uint64 BytesPerSlice = BlocksPerSlice * GPixelFormats[InPixelFormat].BlockBytes; const FOodleTextureVTable* VTable = GetOodleTextureVTable(OodleTextureVersionLatest); for (int32 Slice = 0; Slice < InNumSlices; Slice++) { OodleTex_Surface LinearSurface = {}; LinearSurface.height = InSizeY; LinearSurface.width = InSizeX; LinearSurface.pixels = OutImage.GetPixelPointer(0, 0, Slice); LinearSurface.rowStrideBytes = OutImage.GetBytesPerPixel() * OutImage.GetWidth(); const uint8* SliceBytes = (uint8*)InEncodedData.GetData() + Slice * BytesPerSlice; OodleTex_Err Result = VTable->fp_OodleTex_DecodeBCN_LinearSurfaces(&LinearSurface, 1, DestOoFormat, OodleBCN, SliceBytes, BlocksPerSlice, nullptr); if (Result != OodleTex_Err_OK) { UE_LOG(LogTextureFormatOodle, Error, TEXT("Failed to decode %hs"), (VTable->fp_OodleTex_Err_GetName)(Result)); return false; } } return true; } virtual bool CompressImage(const FImage& InImage, const FTextureBuildSettings& InBuildSettings, const FIntVector3& InMip0Dimensions, int32 InMip0NumSlicesNoDepth, int32 InMipIndex, int32 InMipCount, FStringView DebugTexturePathName, const bool bInHasAlpha, FCompressedImage2D& OutImage) const override { TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.CompressImage); check(InImage.SizeX > 0); check(InImage.SizeY > 0); check(InImage.NumSlices > 0); if ( InImage.SizeX > OODLETEX_MAX_SURFACE_DIMENSION || InImage.SizeY > OODLETEX_MAX_SURFACE_DIMENSION ) { UE_LOG(LogTextureFormatOodle,Error, TEXT("Image larger than OODLETEX_MAX_SURFACE_DIMENSION : %dx%d > %d"), InImage.SizeX,InImage.SizeY, OODLETEX_MAX_SURFACE_DIMENSION ); return false; } FName CompressOodleTextureVersion; if ( InBuildSettings.OodleTextureSdkVersion.IsNone() ) { // legacy texture without version, and no remap is set up in prefs // use default: CompressOodleTextureVersion = OodleTextureSdkVersionToUseIfNone; } else { CompressOodleTextureVersion = ValidateOodleTextureSdkVersion(InBuildSettings.OodleTextureSdkVersion); } const FOodleTextureVTable * VTable = GetOodleTextureVTable(CompressOodleTextureVersion); if (VTable == nullptr) { UE_LOG(LogTextureFormatOodle,Error, TEXT("Unsupported OodleTextureSdkVersion: %s"), *CompressOodleTextureVersion.ToString() ); return false; } // InImage usually comes in as F32 in linear light // (Unreal has just made mips in that format) // (when no processing is needed, eg for VT tiles, it can come in different formats now) // we are run simultaneously on all mips or VT tiles // bHasAlpha = DetectAlphaChannel , scans the A's for non-opaque , in in CompressMipChain // used by AutoDXT bool bHasAlpha = bInHasAlpha; int RDOLambda; OodleTex_EncodeEffortLevel EffortLevel; OodleTex_RDO_UniversalTiling RDOUniversalTiling; OodleTex_BCNFlags BCNFlags; EPixelFormat CompressedPixelFormat; bool bDebugColor; GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat, &RDOLambda, &EffortLevel, &bDebugColor, &RDOUniversalTiling, &BCNFlags, InBuildSettings, bHasAlpha); OodleTex_BC OodleBCN = OodleBCNFromPixelFormat(CompressedPixelFormat); if (OodleBCN == OodleTex_BC_Invalid) { UE_LOG(LogTextureFormatOodle,Fatal, TEXT("Unsupported CompressedPixelFormat for compression: %d"), (int)CompressedPixelFormat ); } if (CompressedPixelFormat == PF_DXT1) { bHasAlpha = false; } FName TextureFormatName = InBuildSettings.TextureFormatName; bool bIsVT = InBuildSettings.bVirtualStreamable; // LogVerbosity 0 : never // LogVerbosity 1 : only large mips // LogVerbosity 2 : always bool bIsLargeMip = InImage.SizeX >= 1024 || InImage.SizeY >= 1024; if ( GlobalFormatConfig.GetLocalDebugConfig().LogVerbosity >= 2 || (GlobalFormatConfig.GetLocalDebugConfig().LogVerbosity && bIsLargeMip) ) { UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle%s %s encode %i x %i x %i to format %s%s (%s) lambda=%i effort=%i "), *CompressOodleTextureVersion.ToString(), RDOLambda ? TEXT("RDO") : TEXT("non-RDO"), InImage.SizeX, InImage.SizeY, InImage.NumSlices, *TextureFormatName.ToString(), bIsVT ? TEXT(" VT") : TEXT(""), *FString((VTable->fp_OodleTex_BC_GetName)(OodleBCN)), RDOLambda, (int)EffortLevel); } // input Image comes in as F32 in linear light // for BC6 we just leave that alone // for all others we must convert to 8 bit to get Gamma correction // Dest Gamma is either sRGB or Linear, never Pow22 EGammaSpace Gamma = InBuildSettings.GetDestGammaSpace(); if ( ( OodleBCN == OodleTex_BC4U || OodleBCN == OodleTex_BC5U || OodleBCN == OodleTex_BC6U ) && Gamma != EGammaSpace::Linear ) { // BC4,5,6 should always be encoded to linear gamma UE_LOG(LogTextureFormatOodle, Display, TEXT("Image format %s (Oodle %s) encoded with non-Linear Gamma"), \ *TextureFormatName.ToString(), *FString((VTable->fp_OodleTex_BC_GetName)(OodleBCN)) ); } ERawImageFormat::Type ImageFormat; OodleTex_PixelFormat OodlePF; bool bNeedsSanitizeFloat16AndSetAlphaOpaqueForBC6H = false; if (OodleBCN == OodleTex_BC6U) { // BC6 is assumed to be a linear-light HDR Image by default // use OodleTex_BCNFlag_BC6_NonRGBData if it is some other kind of data Gamma = EGammaSpace::Linear; /* // can't do this because we support old version back to 2.9.5 // this works only in newer versions of Oodle Texture if ( InImage.Format == ERawImageFormat::R32F ) { ImageFormat = ERawImageFormat::R32F; OodlePF = OodleTex_PixelFormat_1_F32; } else */ if ( InImage.Format == ERawImageFormat::RGBA32F ) { ImageFormat = ERawImageFormat::RGBA32F; OodlePF = OodleTex_PixelFormat_4_F32_RGBA; // (old comment) : // FImageCore::SanitizeFloat16AndSetAlphaOpaqueForBC6H is not needed here // Oodle will convert the F32 to F16 and also clamp in [0,F16_max] (no negatives, no +inf) // -> note this isn't quite true but maintains legacy behavior in this case } else { // use RGBA16F even for formats like BGRE and R32F that don't technically fit in F16 // BC6 will encode them in F16 anyway, so no harm in clamping now // this avoids doing a big RGBA32F surface alloc // (note that Oodle Texture will convert to RGBA32F internally, but that's per-tile) // @todo Oodle : this uses the non-fast-path of CopyImage ! ImageFormat = ERawImageFormat::RGBA16F; OodlePF = OodleTex_PixelFormat_4_F16_RGBA; // if input format was BGRA8 or similar // it can't possibly be out of bounds and need sanitizing // (negatives, inf, nan) switch(InImage.Format) { case ERawImageFormat::RGBA16F: case ERawImageFormat::R16F: case ERawImageFormat::R32F: bNeedsSanitizeFloat16AndSetAlphaOpaqueForBC6H = true; break; default: break; } } } else if ((OodleBCN == OodleTex_BC4U || OodleBCN == OodleTex_BC5U) && Gamma == EGammaSpace::Linear && !bDebugColor) { // for BC4/5 use 16-bit integer U16 pixels : // BC4/5 should always have linear gamma /* // @todo Oodle: allow 8-bit to be passed directly to Oodle without converting to 16 bit (and bump DDC key) if ( InImage.Format == ERawImageFormat::BGRA8 && InBuildSettings.bUseNewMipFilter ) { ImageFormat = ERawImageFormat::BGRA8; OodlePF = OodleTex_PixelFormat_4_U8_BGRA; } else */ if ( InImage.Format == ERawImageFormat::RGBA16 ) { ImageFormat = ERawImageFormat::RGBA16; OodlePF = OodleTex_PixelFormat_4_U16; } else { ImageFormat = ERawImageFormat::BGRA8; // <- not really, 2_U16 in disguise! OodlePF = OodleTex_PixelFormat_2_U16; } } else { ImageFormat = ERawImageFormat::BGRA8; // if bHasAlpha is off due to bForceNoAlphaChannel, we could still have transparent pixels // force Oodle to read input alpha as opaque : OodlePF = bHasAlpha ? OodleTex_PixelFormat_4_U8_BGRA : OodleTex_PixelFormat_4_U8_BGRx; } bool bIsSpecial2U16 = (OodlePF == OodleTex_PixelFormat_2_U16); bool bNeedsImageCopy = ImageFormat != InImage.Format || Gamma != InImage.GammaSpace || (CompressedPixelFormat == PF_DXT5 && TextureFormatName == GTextureFormatNameDXT5n) || bDebugColor || bIsSpecial2U16; FImage ImageCopy; if (bNeedsImageCopy) { TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.FormatChange); //not sure if we should bill this alloc to OodleTexture or the calling context (TextureCompressor) //we are freeing the previous Image alloc to replace it with a changed format //LLM_SCOPE_BYTAG(OodleTexture); if ( bIsSpecial2U16 ) { ImageCopy.Init(InImage.SizeX,InImage.SizeY,InImage.NumSlices,ImageFormat,EGammaSpace::Linear); FImageCore::CopyImageTo2U16(InImage,ImageCopy); // everything past this point only uses OodlePF // so the fact that ImageCopy.Format is wrong is okay } else { InImage.CopyTo(ImageCopy, ImageFormat, Gamma); } // after we copy the image, we can free the source // can reduce peak mem use to do so immediately // (source is usually/often F32 RGBA (when not VT) so quite fat) // (detached) const_cast(InImage).FreeData(true); } const FImage& Image = bNeedsImageCopy ? ImageCopy : InImage; if ( bNeedsSanitizeFloat16AndSetAlphaOpaqueForBC6H ) { FImageCore::SanitizeFloat16AndSetAlphaOpaqueForBC6H(const_cast(Image)); } // verify OodlePF matches Image : check( Image.GetBytesPerPixel() == (VTable->fp_OodleTex_PixelFormat_BytesPerPixel)(OodlePF) ); SSIZE_T InRowStrideBytes = Image.GetBytesPerPixel() * Image.SizeX; SSIZE_T InBytesPerSlice = InRowStrideBytes * Image.SizeY; uint8 * ImageBasePtr = (uint8 *) &(Image.RawData[0]); SSIZE_T InBytesTotal = InBytesPerSlice * Image.NumSlices; check( Image.RawData.Num() == InBytesTotal ); if ( CompressedPixelFormat == PF_DXT5 && TextureFormatName == GTextureFormatNameDXT5n) { // this is only used if Compat.UseDXT5NormalMaps // normal map comes in as RG , B&A can be ignored // in the optional use BC5 path, only the source RG pass through // normal was in RG , move to GA if ( OodlePF == OodleTex_PixelFormat_4_U8_BGRx ) { OodlePF = OodleTex_PixelFormat_4_U8_BGRA; } check( OodlePF == OodleTex_PixelFormat_4_U8_BGRA ); for(uint8 * ptr = ImageBasePtr; ptr < (ImageBasePtr + InBytesTotal); ptr += 4) { // ptr is BGRA ptr[3] = ptr[2]; // match what NVTT does, it sets R=FF and B=0 // NVTT also sets weight=0 for B so output B is undefined // but output R is preserved at 1.f ptr[0] = 0xFF; ptr[2] = 0; } } if ( bDebugColor ) { // fill Texture with solid color based on which BCN we would have output // checker board if RDO // lets you visually identify BCN textures in the Editor or game const bool IsRDO = RDOLambda != 0; constexpr SSIZE_T CheckerSizeBits = 4; const SSIZE_T NumSlices = Image.NumSlices; const SSIZE_T SizeY = Image.SizeY; const SSIZE_T SizeX = Image.SizeX; // use fast encoding settings for debug color : RDOLambda = 0; EffortLevel = OodleTex_EncodeEffortLevel_Low; if ( OodlePF == OodleTex_PixelFormat_4_F32_RGBA ) { //BC6 = purple check(OodleBCN == OodleTex_BC6U); if (IsRDO) { // debug color with checker board for (SSIZE_T Slice = 0; Slice < NumSlices; Slice++) { float* LineBase = (float*)(ImageBasePtr + InBytesPerSlice * Slice); for (SSIZE_T Y = 0; Y < SizeY; Y++, LineBase += (InRowStrideBytes / sizeof(float))) { for (SSIZE_T X = 0; X < SizeX; X++) { float* Pixel = LineBase + 4 * X; SSIZE_T GridOnY = Y & (1 << CheckerSizeBits); SSIZE_T GridOnX = X & (1 << CheckerSizeBits); SSIZE_T GridOn = GridOnX ^ GridOnY; if (GridOn) { Pixel[0] = 0.5f; Pixel[1] = 0; Pixel[2] = 0.8f; Pixel[3] = 1.f; } else { Pixel[0] = 1.0f; Pixel[1] = 1.0f; Pixel[2] = 1.0f; Pixel[3] = 1.f; } } } // each line } // each slice } // end if RDO else { for(float * ptr = (float *) ImageBasePtr; ptr < (float *)(ImageBasePtr + InBytesTotal); ptr += 4) { // RGBA floats ptr[0] = 0.5f; ptr[1] = 0; ptr[2] = 0.8f; ptr[3] = 1.f; } } } else { check( OodlePF == OodleTex_PixelFormat_4_U8_BGRA || OodlePF == OodleTex_PixelFormat_4_U8_BGRx ); // BGRA in bytes uint32 DebugColor = 0xFF000000U; // alpha switch(OodleBCN) { case OodleTex_BC1_WithTransparency: case OodleTex_BC1: DebugColor |= 0xFF0000; break; // BC1 = red case OodleTex_BC2: DebugColor |= 0x008000; break; // BC2/3 = greens case OodleTex_BC3: DebugColor |= 0x00FF00; break; case OodleTex_BC4S: case OodleTex_BC4U: DebugColor |= 0x808000; break; // BC4/5 = yellows case OodleTex_BC5S: case OodleTex_BC5U: DebugColor |= 0xFFFF00; break; case OodleTex_BC7RGB: DebugColor |= 0x8080FF; break; // BC7 = blues case OodleTex_BC7RGBA: DebugColor |= 0x0000FF; break; default: break; } if (IsRDO) { // debug color with checker board for (SSIZE_T Slice = 0; Slice < NumSlices; Slice++) { uint8* LineBase = ImageBasePtr + InBytesPerSlice * Slice;; for (SSIZE_T Y = 0; Y < SizeY; Y++, LineBase += InRowStrideBytes) { for (SSIZE_T X = 0; X < SizeX; X++) { uint8* Pixel = LineBase + 4 * X; SSIZE_T GridOnY = Y & (1 << CheckerSizeBits); SSIZE_T GridOnX = X & (1 << CheckerSizeBits); SSIZE_T GridOn = GridOnX ^ GridOnY; if (GridOn) { *((uint32*)Pixel) = DebugColor; } else { *((uint32*)Pixel) = 0xFF000000; } } } // each line } // each slice } // end if RDO else { for(uint8 * ptr = ImageBasePtr; ptr < (ImageBasePtr + InBytesTotal); ptr += 4) { *((uint32 *)ptr) = DebugColor; } } } } /* UE_LOG(LogTextureFormatOodle, Display, TEXT("bHasAlpha=%d OodlePF=%d=%s OodleBCN=%d=%s"), (int)bHasAlpha, (int)OodlePF, *FString((VTable->fp_OodleTex_PixelFormat_GetName)(OodlePF)), (int)OodleBCN, *FString((VTable->fp_OodleTex_BC_GetName)(OodleBCN)) ); */ int BytesPerBlock = (VTable->fp_OodleTex_BC_BytesPerBlock)(OodleBCN); int NumBlocksX = (Image.SizeX + 3)/4; int NumBlocksY = (Image.SizeY + 3)/4; OO_SINTa NumBlocksPerSlice = NumBlocksX * NumBlocksY; OO_SINTa OutBytesPerSlice = NumBlocksPerSlice * BytesPerBlock; OO_SINTa OutBytesTotal = OutBytesPerSlice * Image.NumSlices; OutImage.PixelFormat = CompressedPixelFormat; // old behavior : //OutImage.SizeX = NumBlocksX*4; //OutImage.SizeY = NumBlocksY*4; OutImage.SizeX = Image.SizeX; OutImage.SizeY = Image.SizeY; OutImage.NumSlicesWithDepth = Image.NumSlices; { TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.Alloc); OutImage.RawData.AddUninitialized(OutBytesTotal); } UE_LOG(LogTextureFormatOodle, Verbose, TEXT("TFO out size=%dx%d stride=%d total=%" PTRINT_FMT), OutImage.SizeX,OutImage.SizeY, NumBlocksX * BytesPerBlock, OutBytesTotal); uint8 * OutBlocksBasePtr = (uint8 *) &OutImage.RawData[0]; // Check if we want to dump the before/after images out. bool bImageDump = false; if (GlobalFormatConfig.GetLocalDebugConfig().DebugDumpFilter.Len() && !bDebugColor && // don't bother if they are solid color (Image.SizeX >= 4 || Image.SizeY >= 4)) // don't bother if they are too small. { if (FWildcardString::IsMatchSubstring(*GlobalFormatConfig.GetLocalDebugConfig().DebugDumpFilter, DebugTexturePathName.GetData(), DebugTexturePathName.GetData() + DebugTexturePathName.Len(), ESearchCase::IgnoreCase)) { bImageDump = true; } } int CurJobifyNumThreads = OodleJobifyNumThreads; void* CurJobifyUserPointer = OodleJobifyUserPointer; // CurJobifyUserPointer is not used in the Unreal TaskGraph // so we can use it to pass a pointer to a debug info struct: FOodleJobDebugInfo LocalDebugInfo; LocalDebugInfo.DebugTexturePathName = DebugTexturePathName; LocalDebugInfo.SizeX = Image.SizeX; LocalDebugInfo.SizeY = Image.SizeY; LocalDebugInfo.OodleBCN = OodleBCN; LocalDebugInfo.RDOLambda = RDOLambda; if ( ! OodleJobifyUseExampleJobify ) { check( CurJobifyUserPointer == nullptr ); CurJobifyUserPointer = (void *) &LocalDebugInfo; } // Have a target number of pixels per job, and clamp the num threads // to avoid generating lots of tiny jobs const int64 TargetPixelsPerJobThread = 128 * 128; const int TargetJobThreads = (int)(int64(Image.SizeX) * Image.SizeY / TargetPixelsPerJobThread); if (bIsVT || TargetJobThreads <= 1) { // VT runs its tiles in a ParallelFor on the TaskGraph // We internally also make tasks on TaskGraph // it should not deadlock to do tasks from tasks, but it's not handled well // parallelism at the VT tile level only works better // disable our own internal threading for VT tiles : CurJobifyNumThreads = OODLETEX_JOBS_DISABLE; CurJobifyUserPointer = nullptr; } else { CurJobifyNumThreads = FMath::Min(CurJobifyNumThreads, TargetJobThreads); } // encode each slice std::atomic_bool bCompressionSucceeded { true }; ParallelFor( TEXT("ProcessSlice"), Image.NumSlices, 1, [&](int32 Slice) { // Early out in case another task failed if (!bCompressionSucceeded) { return; } OodleTex_Surface InSurf = {}; InSurf.pixels = ImageBasePtr + Slice * InBytesPerSlice; InSurf.rowStrideBytes = InRowStrideBytes; InSurf.width = Image.SizeX; InSurf.height = Image.SizeY; uint8 * OutSlicePtr = OutBlocksBasePtr + Slice * OutBytesPerSlice; // verify that the surface memory ranges given to us by Unreal are valid before calling into Oodle Texture: CheckMemoryIsReadable(InSurf.pixels,InBytesPerSlice); CheckMemoryIsReadable(OutSlicePtr,OutBytesPerSlice); OodleTex_RDO_Options OodleOptions = { }; OodleOptions.effort = EffortLevel; OodleOptions.metric = OodleTex_RDO_ErrorMetric_Default; OodleOptions.bcn_flags = BCNFlags; OodleOptions.universal_tiling = RDOUniversalTiling; if (bImageDump) { DebugDumpDDS(DebugTexturePathName,InSurf.width,InSurf.height,Slice, DXGIFormatFromOodlePF(OodlePF),TEXT("IN"), InSurf.pixels, InBytesPerSlice); } { TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.EncodeBCN); // if RDOLambda == 0, does non-RDO encode : OodleTex_Err OodleErr = (VTable->fp_OodleTex_EncodeBCN_RDO_Ex)(OodleBCN, OutSlicePtr, NumBlocksPerSlice, &InSurf, 1, OodlePF, NULL, RDOLambda, &OodleOptions, CurJobifyNumThreads, CurJobifyUserPointer); if (OodleErr != OodleTex_Err_OK) { const char * OodleErrStr = (VTable->fp_OodleTex_Err_GetName)(OodleErr); UE_LOG(LogTextureFormatOodle, Warning, TEXT("Oodle Texture encode failed!? %d=%s"), (int)OodleErr, ANSI_TO_TCHAR(OodleErrStr) ); bCompressionSucceeded = false; return; } } if (bImageDump) { // put RDO lambda on the debug name : FStringView DebugNameOut = DebugTexturePathName; FString Scratch; if ( RDOLambda != 0 ) { Scratch = FString::Printf(TEXT("%.*s_RDO%d"), DebugTexturePathName.Len(), DebugTexturePathName.GetData(), RDOLambda); DebugNameOut = Scratch; } DebugDumpDDS(DebugNameOut,InSurf.width,InSurf.height,Slice, DXGIFormatFromOodleBC(OodleBCN),TEXT("OUT"), OutSlicePtr, OutBytesPerSlice); } }, EParallelForFlags::Unbalanced ); return bCompressionSucceeded; } // noinline so we see it on the call stack : FORCENOINLINE void CheckMemoryIsReadable(const void * Buffer,int64 Size) const { check( Size > 0 ); const uint8 * Start = (const uint8 *)Buffer; // volatile to ensure it's not optimized out volatile uint8 Byte; Byte = Start[0]; Byte = Start[Size-1]; } }; //=============================================================== // TFO_ plugins to Oodle to run Oodle system services in Unreal // @todo Oodle : factor this out and share for Core & Net some day static OO_U64 OODLE_CALLBACK TFO_RunJob(t_fp_Oodle_Job* JobFunction, void* JobData, OO_U64* Dependencies, int NumDependencies, void* UserPtr) { using namespace UE::Tasks; // DebugInfo to inspect: const FOodleJobDebugInfo * DebugInfo = (FOodleJobDebugInfo *)UserPtr; FTask* Task = new FTask; Task->Launch( TEXT("TFOodle.EncodeBCN_Task"), [JobFunction, JobData] { TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.EncodeBCN_Task); JobFunction(JobData); }, TArrayView{ reinterpret_cast(Dependencies), NumDependencies }, // Use Background priority so we don't use Foreground time in the Editor // @todo maybe it's better to inherit so the outer caller can tell us if we are high priority or not? IsInGameThread() ? ETaskPriority::Normal : ETaskPriority::BackgroundNormal ); return reinterpret_cast(Task); } static void OODLE_CALLBACK TFO_WaitJob(OO_U64 JobHandle, void* UserPtr) { using namespace UE::Tasks; // DebugInfo to inspect: const FOodleJobDebugInfo * DebugInfo = (FOodleJobDebugInfo *)UserPtr; TRACE_CPUPROFILER_EVENT_SCOPE(TFOodle.WaitJob); FTask* Task = reinterpret_cast(JobHandle); Task->Wait(); delete Task; } static OO_BOOL OODLE_CALLBACK TFO_OodleAssert(const char* file, const int line, const char* function, const char* message) { // AssertFailed exits the program FDebug::AssertFailed(message, file, line); // return true to issue a debug break at the execution site return true; } static void OODLE_CALLBACK TFO_OodleLog(int verboseLevel, const char* file, int line, const char* InFormat, ...) { ANSICHAR TempString[1024]; va_list Args; va_start(Args, InFormat); FCStringAnsi::GetVarArgs(TempString, UE_ARRAY_COUNT(TempString), InFormat, Args); va_end(Args); UE_LOG_CLINKAGE(LogTextureFormatOodle, Display, TEXT("Oodle Log: %s"), ANSI_TO_TCHAR(TempString)); } static void* OODLE_CALLBACK TFO_OodleMallocAligned(OO_SINTa Bytes, OO_S32 Alignment) { LLM_SCOPE_BYTAG(OodleTexture); void * Ret = FMemory::Malloc(Bytes, Alignment); check( Ret != nullptr ); return Ret; } static void OODLE_CALLBACK TFO_OodleFree(void* ptr) { FMemory::Free(ptr); } // Init is only done once for all versions static void TFO_Plugins_Init() { // Install Unreal system plugins to OodleTex // this should only be done once // and should be done before any other Oodle calls // plugins to Core/Tex/Net are independent GConfig->GetBool(TEXT("TextureFormatOodleSettings"), TEXT("UseOodleExampleJobify"), OodleJobifyUseExampleJobify, GEngineIni); if (OodleJobifyUseExampleJobify) { UE_LOG(LogTextureFormatOodle, Display, TEXT("Using Oodle Example Jobify")); // Optionally we allow for users to use the internal Oodle job system instead of // thunking to the Unreal task graph. OodleJobifyUserPointer = example_jobify_init(); OodleJobifyNumThreads = example_jobify_target_parallelism; } else { OodleJobifyUserPointer = nullptr; OodleJobifyNumThreads = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads()); } } // Install is done for each Oodle DLL static void TFO_Plugins_Install(const FOodleTextureVTable * VTable) { if (OodleJobifyUseExampleJobify) { (VTable->fp_OodleTex_Plugins_SetJobSystemAndCount)(example_jobify_run_job_fptr, example_jobify_wait_job_fptr, example_jobify_target_parallelism); } else { (VTable->fp_OodleTex_Plugins_SetJobSystemAndCount)(TFO_RunJob, TFO_WaitJob, OodleJobifyNumThreads); } (VTable->fp_OodleTex_Plugins_SetAssertion)(TFO_OodleAssert); (VTable->fp_OodleTex_Plugins_SetPrintf)(TFO_OodleLog); (VTable->fp_OodleTex_Plugins_SetAllocators)(TFO_OodleMallocAligned, TFO_OodleFree); } //========================================================================= class FTextureFormatOodleModule : public ITextureFormatModule { public: ITextureFormat* TextureFormat = nullptr; FTextureFormatOodleModule() { } virtual ~FTextureFormatOodleModule() { if ( TextureFormat ) { delete TextureFormat; TextureFormat = nullptr; } } virtual void StartupModule() override { } virtual bool CanCallGetTextureFormats() override { return false; } virtual ITextureFormat* GetTextureFormat() { // this is called several times during normal init auto MakeTextureFormat = [&]() { check( TextureFormat == nullptr ); FTextureFormatOodle * ptr = new FTextureFormatOodle(); if ( ptr->Init() ) { TextureFormat = ptr; } else { UE_LOG(LogTextureFormatOodle,Warning,TEXT("Oodle Texture Init failed, not installed")); delete ptr; } }; UE_CALL_ONCE( MakeTextureFormat ); return TextureFormat; } static inline UE::DerivedData::TBuildFunctionFactory BuildFunctionFactory; static inline UE::DerivedData::TBuildFunctionFactory> DecodeBuildFunctionFactory; }; IMPLEMENT_MODULE(FTextureFormatOodleModule, TextureFormatOodle);