You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
1313 lines
46 KiB
C++
1313 lines
46 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "TextureFormatOodlePCH.h"
|
|
#include "CoreMinimal.h"
|
|
#include "ImageCore.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/TaskGraphInterfaces.h"
|
|
#include "IImageWrapper.h"
|
|
#include "IImageWrapperModule.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Serialization/CompactBinary.h"
|
|
#include "Serialization/CompactBinaryWriter.h"
|
|
#include "TextureBuildFunction.h"
|
|
#include "DerivedDataBuildFunctionFactory.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
|
|
|
|
by default this plugin enables RDO with a moderate quality level (lambda=30).
|
|
Set DefaultRDOLambda=40 for smaller compressed sizes.
|
|
|
|
quality can be controlled at three levels :
|
|
|
|
1. Each Texture can choose an individual setting with LossyCompressionAmount
|
|
2. If that is "Default", the setting is looked up in the LODGroup
|
|
3. If that is not set, the global default is used (DefaultRDOLambda)
|
|
|
|
Lambda up to 40 usually produces very high quality. The need to for per-Texture adjustment should be rare
|
|
and is mainly for when textures are used in unusual ways (not just diffuse color or normal maps).
|
|
|
|
Oodle Texture can encode BC1-7. It does not currently encode ASTC or other mobile formats.
|
|
|
|
=====================
|
|
|
|
TextureFormatOodle handles formats OODLE_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="OODLE_"
|
|
|
|
When this is enabled, the formats like "DXT1" are renamed to "OODLE_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]
|
|
bForceAllBC23ToBC7=False
|
|
bForceRDOOff_Editor=False
|
|
bForceRDOOff_NoEditor=False
|
|
bDebugColor=False
|
|
DefaultRDOLambda=30
|
|
GlobalLambdaMultiplier=1.0
|
|
CompressEffortLevel_NoEditor=High
|
|
CompressEffortLevel_Editor=Normal
|
|
|
|
The sense of the bools is set so that all-false is default behavior.
|
|
|
|
TextureFormatOodle by default tries to exactly reproduce the legacy behavior of
|
|
TextureFormatDXT+TextureFormatISPC , just with Oodle Texture RDO encoding.
|
|
|
|
The behavior of the options is :
|
|
|
|
CompressEffortLevel_NoEditor :
|
|
CompressEffortLevel_Editor :
|
|
|
|
Sets how much time Oodle should spend finding good results. Values are
|
|
from the OodleTex_EncodeEffortLevel enum - Default, Low, Normal, High.
|
|
|
|
bForceAllBC23ToBC7 :
|
|
|
|
If true, all BC2 & 3 (DXT3 and DXT5) is encoded to BC7 instead.
|
|
|
|
On DX11 games, BC7 usualy has higher quality and takes the same space in memory as BC3.
|
|
|
|
For example in Unreal, "AutoDXT" selects DXT1 (BC1) for opaque textures and DXT5 (BC3)
|
|
for textures with alpha. If you turn on this option, the BC3 will change to BC7, so
|
|
"AutoDXT" will now select BC1 for opaque and BC7 for alpha.
|
|
|
|
It is off by default to make default behavior match the old encoders.
|
|
|
|
bForceRDOOff_Editor :
|
|
bForceRDOOff_NoEditor :
|
|
|
|
Force Oodle Texture to use non-RDO encoding. This sets lambda to 0 for all encodes.
|
|
(this is different than setting DefaultRDOLambda=0 because it also applies to textures
|
|
that have per-texture lambda overrides set). When EditorOnly data is present _Editor
|
|
is used to facilitate iteration times.
|
|
|
|
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).
|
|
|
|
DefaultRDOLambda :
|
|
|
|
global default lambda value that is used if no per-texture lambda is set.
|
|
(see next section)
|
|
|
|
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 :
|
|
|
|
|
|
[/Script/Engine.TextureLODSettings]
|
|
@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 "DefaultRDOLambda" from our
|
|
INI block. eg. for "WorldSpecular" above it would use the DefaultRDOLambda setting.
|
|
|
|
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 (if bEnableInEditor is true).
|
|
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);
|
|
|
|
// user data passed to Oodle Jobify system
|
|
static int OodleJobifyNumThreads = 0;
|
|
static void *OodleJobifyUserPointer = nullptr;
|
|
|
|
// 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 OODLE_ prefixed names like "OODLE_DXT1"
|
|
#define DECL_FORMAT_NAME(FormatName) static FName GTextureFormatName##FormatName = FName(TEXT("OODLE_" #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 FImageDumper
|
|
{
|
|
|
|
public:
|
|
|
|
FImageDumper()
|
|
: ImageWrapperModule(nullptr)
|
|
, ImageFormat(EImageFormat::Invalid)
|
|
, RGBFormat(ERGBFormat::Invalid)
|
|
, BytesPerPixel(0)
|
|
, BitDepth(0)
|
|
, Extension(nullptr)
|
|
{ }
|
|
|
|
bool Initialize(const ERawImageFormat::Type InImageFormat)
|
|
{
|
|
ImageWrapper.Reset();
|
|
|
|
switch (InImageFormat)
|
|
{
|
|
case ERawImageFormat::RGBA32F:
|
|
ImageFormat = EImageFormat::EXR;
|
|
RGBFormat = ERGBFormat::RGBAF;
|
|
BytesPerPixel = 16;
|
|
BitDepth = 32;
|
|
Extension = TEXT(".exr");
|
|
break;
|
|
|
|
case ERawImageFormat::RGBA16:
|
|
ImageFormat = EImageFormat::PNG;
|
|
RGBFormat = ERGBFormat::RGBA;
|
|
BytesPerPixel = 8;
|
|
BitDepth = 16;
|
|
Extension = TEXT(".png");
|
|
break;
|
|
|
|
case ERawImageFormat::BGRA8:
|
|
ImageFormat = EImageFormat::PNG;
|
|
RGBFormat = ERGBFormat::BGRA;
|
|
BytesPerPixel = 4;
|
|
BitDepth = 8;
|
|
Extension = TEXT(".png");
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (!ImageWrapperModule)
|
|
{
|
|
ImageWrapperModule = FModuleManager::GetModulePtr<IImageWrapperModule>("ImageWrapper");
|
|
}
|
|
|
|
if (ImageWrapperModule)
|
|
{
|
|
ImageWrapper = ImageWrapperModule->CreateImageWrapper(ImageFormat);
|
|
}
|
|
|
|
return ImageWrapper.IsValid();
|
|
}
|
|
|
|
bool DumpImage(const void* InRawData, int64 InRawSize, const int32 InWidth, const int32 InHeight, const int32 InSlice, const int32 InRDOLambda, const OodleTex_BC InOodleBCN)
|
|
{
|
|
check(InRawData);
|
|
check(InWidth > 0);
|
|
check(InHeight > 0);
|
|
check(InRawSize == (int64)BytesPerPixel * InWidth * InHeight);
|
|
|
|
if (!ImageWrapper.IsValid() || !ImageWrapper->SetRaw(InRawData, InRawSize, InWidth, InHeight, RGBFormat, BitDepth))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FMD5 MD5;
|
|
FString ImageHash = MD5.HashBytes(static_cast<const uint8*>(InRawData), InRawSize);
|
|
FString OodleBCName(OodleTex_BC_GetName(InOodleBCN));
|
|
FString Filename = FString::Printf(TEXT("%s.w%d.h%d.s%d.rdo%d.%s%s"), *ImageHash, InWidth, InHeight, InSlice, InRDOLambda, *OodleBCName, Extension);
|
|
|
|
// put in subdir by format and size
|
|
// helps reduce the count of files in a single dir, which stresses the file system
|
|
FString Subdir = FString::Printf(TEXT("%s.w%d.h%d"), *OodleBCName, InWidth, InHeight);
|
|
|
|
FString Path = FPaths::ProjectSavedDir() / TEXT("Oodle") / TEXT("DebugDump") / Subdir / Filename;
|
|
|
|
//UE_LOG(LogTextureFormatOodle, Display, TEXT("DumpImage : %s"), *Filename );
|
|
|
|
const TArray64<uint8>& CompressedImage = ImageWrapper->GetCompressed((int32)EImageCompressionQuality::Uncompressed);
|
|
return FFileHelper::SaveArrayToFile(CompressedImage, *Path);
|
|
}
|
|
|
|
private:
|
|
|
|
IImageWrapperModule* ImageWrapperModule;
|
|
TSharedPtr<IImageWrapper> ImageWrapper;
|
|
|
|
EImageFormat ImageFormat;
|
|
ERGBFormat RGBFormat;
|
|
int32 BytesPerPixel;
|
|
int32 BitDepth;
|
|
const TCHAR* Extension;
|
|
};
|
|
|
|
class FTextureFormatOodleConfig
|
|
{
|
|
public:
|
|
struct FLocalDebugConfig
|
|
{
|
|
FLocalDebugConfig() :
|
|
bDebugDump(false),
|
|
LogVerbosity(0)
|
|
{
|
|
}
|
|
|
|
bool bDebugDump; // dump textures that were encoded
|
|
int LogVerbosity; // 0-2 ; 0=never, 1=large only, 2=always
|
|
};
|
|
|
|
FTextureFormatOodleConfig() :
|
|
bForceAllBC23ToBC7(false),
|
|
bForceRDOOff_NoEditor(true),
|
|
bForceRDOOff_Editor(true),
|
|
CompressEffortLevel_NoEditor(OodleTex_EncodeEffortLevel_High),
|
|
CompressEffortLevel_Editor(OodleTex_EncodeEffortLevel_Normal),
|
|
RDOUniversalTiling(OodleTex_RDO_UniversalTiling_Disable),
|
|
bDebugColor(false),
|
|
DefaultRDOLambda(OodleTex_RDOLagrangeLambda_Default),
|
|
GlobalLambdaMultiplier(1.f)
|
|
{
|
|
}
|
|
|
|
const FLocalDebugConfig& GetLocalDebugConfig() const
|
|
{
|
|
return LocalDebugConfig;
|
|
}
|
|
|
|
static const TCHAR* EffortLevelToString(OodleTex_EncodeEffortLevel InEffortLevel)
|
|
{
|
|
switch (InEffortLevel)
|
|
{
|
|
case OodleTex_EncodeEffortLevel_Default:
|
|
{
|
|
return TEXT("Default");
|
|
}
|
|
case OodleTex_EncodeEffortLevel_Low:
|
|
{
|
|
return TEXT("Low");
|
|
}
|
|
case OodleTex_EncodeEffortLevel_Normal:
|
|
{
|
|
return TEXT("Normal");
|
|
}
|
|
case OodleTex_EncodeEffortLevel_High:
|
|
{
|
|
return TEXT("High");
|
|
}
|
|
}
|
|
UE_LOG(LogTextureFormatOodle, Warning, TEXT("Invalid Oodle effort level passed to ToString: %d -- returning \"Default\""), InEffortLevel);
|
|
return TEXT("Default");
|
|
}
|
|
|
|
static bool EffortLevelFromConfig(const TCHAR* InSection, const TCHAR* InKey, OodleTex_EncodeEffortLevel& OutEffortLevel)
|
|
{
|
|
FString EffortLevel_String;
|
|
if (GConfig->GetString(InSection, InKey, EffortLevel_String, GEngineIni))
|
|
{
|
|
OodleTex_EncodeEffortLevel Result;
|
|
if (EffortLevel_String.Compare(TEXT("Default"), ESearchCase::IgnoreCase) == 0)
|
|
{
|
|
Result = OodleTex_EncodeEffortLevel_Default;
|
|
}
|
|
else if (EffortLevel_String.Compare(TEXT("Low"), ESearchCase::IgnoreCase) == 0)
|
|
{
|
|
Result = OodleTex_EncodeEffortLevel_Low;
|
|
}
|
|
else if (EffortLevel_String.Compare(TEXT("Normal"), ESearchCase::IgnoreCase) == 0)
|
|
{
|
|
Result = OodleTex_EncodeEffortLevel_Normal;
|
|
}
|
|
else if (EffortLevel_String.Compare(TEXT("High"), ESearchCase::IgnoreCase) == 0)
|
|
{
|
|
Result = OodleTex_EncodeEffortLevel_High;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTextureFormatOodle, Warning, TEXT("Invalid %s specified: %s, using defaults."), InKey, *EffortLevel_String);
|
|
return false;
|
|
}
|
|
OutEffortLevel = Result;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ImportFromConfigCache()
|
|
{
|
|
const TCHAR* IniSection = TEXT("TextureFormatOodleSettings");
|
|
|
|
#if 0
|
|
// Check that our config section exists, and if not, init with defaults
|
|
// this will add it to your per-user "Saved" Engine.ini
|
|
// eg: C:\UnrealEngine\Games\oodletest\Saved\Config\Windows\Engine.ini
|
|
// you can then move or copy it to DefaultEngine.ini if you like
|
|
if (!GConfig->DoesSectionExist(OODLETEXTURE_INI_SECTION, GEngineIni))
|
|
{
|
|
GConfig->SetBool(OODLETEXTURE_INI_SECTION, TEXT("bForceAllBC23ToBC7"), bForceAllBC23ToBC7, GEngineIni);
|
|
GConfig->SetBool(OODLETEXTURE_INI_SECTION, TEXT("bForceRDOOff_NoEditor"), bForceRDOOff_NoEditor, GEngineIni);
|
|
GConfig->SetBool(OODLETEXTURE_INI_SECTION, TEXT("bForceRDOOff_Editor"), bForceRDOOff_Editor, GEngineIni);
|
|
GConfig->SetString(OODLETEXTURE_INI_SECTION, TEXT("CompressEffortLevel_NoEditor"), EffortLevelToString(CompressEffortLevel_NoEditor), GEngineIni);
|
|
GConfig->SetString(OODLETEXTURE_INI_SECTION, TEXT("CompressEffortLevel_Editor"), EffortLevelToString(CompressEffortLevel_Editor), GEngineIni);
|
|
GConfig->SetBool(OODLETEXTURE_INI_SECTION, TEXT("bDebugColor"), bDebugColor, GEngineIni);
|
|
GConfig->SetBool(OODLETEXTURE_INI_SECTION, TEXT("bDebugDump"), bDebugDump, GEngineIni);
|
|
GConfig->SetInt(OODLETEXTURE_INI_SECTION, TEXT("LogVerbosity"), LogVerbosity, GEngineIni);
|
|
GConfig->SetFloat(OODLETEXTURE_INI_SECTION, TEXT("GlobalLambdaMultiplier"), GlobalLambdaMultiplier, GEngineIni);
|
|
GConfig->SetInt(OODLETEXTURE_INI_SECTION, TEXT("DefaultRDOLambda"), DefaultRDOLambda, GEngineIni);
|
|
GConfig->SetInt(OODLETEXTURE_INI_SECTION, TEXT("RDOUniversalTiling"), (int32)RDOUniversalTiling, GEngineIni);
|
|
|
|
GConfig->Flush(false);
|
|
}
|
|
#endif
|
|
|
|
//
|
|
// 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("bForceAllBC23ToBC7"), bForceAllBC23ToBC7, GEngineIni);
|
|
GConfig->GetBool(IniSection, TEXT("bForceRDOOff_NoEditor"), bForceRDOOff_NoEditor, GEngineIni);
|
|
GConfig->GetBool(IniSection, TEXT("bForceRDOOff_Editor"), bForceRDOOff_Editor, GEngineIni);
|
|
GConfig->GetBool(IniSection, TEXT("bDebugColor"), bDebugColor, GEngineIni);
|
|
GConfig->GetBool(IniSection, TEXT("bDebugDump"), LocalDebugConfig.bDebugDump, GEngineIni);
|
|
GConfig->GetInt(IniSection, TEXT("LogVerbosity"), LocalDebugConfig.LogVerbosity, GEngineIni);
|
|
GConfig->GetFloat(IniSection, TEXT("GlobalLambdaMultiplier"), GlobalLambdaMultiplier, GEngineIni);
|
|
GConfig->GetInt(IniSection, TEXT("DefaultRDOLambda"), DefaultRDOLambda, GEngineIni);
|
|
GConfig->GetInt(IniSection, TEXT("RDOUniversalTiling"), (int32&)RDOUniversalTiling, GEngineIni);
|
|
|
|
EffortLevelFromConfig(IniSection, TEXT("CompressEffortLevel_NoEditor"), CompressEffortLevel_NoEditor);
|
|
EffortLevelFromConfig(IniSection, TEXT("CompressEffortLevel_Editor"), CompressEffortLevel_Editor);
|
|
|
|
// sanitize config values :
|
|
DefaultRDOLambda = FMath::Clamp(DefaultRDOLambda,0,100);
|
|
|
|
if ( GlobalLambdaMultiplier <= 0.f )
|
|
{
|
|
GlobalLambdaMultiplier = 1.f;
|
|
}
|
|
|
|
if (RDOUniversalTiling < 0 ||
|
|
RDOUniversalTiling > OodleTex_RDO_UniversalTiling_Max)
|
|
{
|
|
UE_LOG(LogTextureFormatOodle, Warning, TEXT("Invalid RDOUniversalTiling value supplied: %d, using 0 (Disabled)"), RDOUniversalTiling);
|
|
RDOUniversalTiling = OodleTex_RDO_UniversalTiling_Disable;
|
|
}
|
|
|
|
UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle Texture %s init {cook RDO %s %s, Editor RDO %s %s} with DefaultRDOLambda=%d, RDOUniversalTiling=%d"),
|
|
TEXT(OodleTextureVersion),
|
|
bForceRDOOff_NoEditor ? TEXT("Off") : TEXT("On"), EffortLevelToString(CompressEffortLevel_NoEditor),
|
|
bForceRDOOff_Editor ? TEXT("Off") : TEXT("On"), EffortLevelToString(CompressEffortLevel_Editor),
|
|
DefaultRDOLambda,
|
|
(int32)RDOUniversalTiling
|
|
);
|
|
#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
|
|
{
|
|
int RDOLambda;
|
|
OodleTex_EncodeEffortLevel EffortLevel;
|
|
OodleTex_RDO_UniversalTiling LocalRDOUniversalTiling;
|
|
EPixelFormat CompressedPixelFormat;
|
|
bool bLocalDebugColor;
|
|
const bool bHasAlpha = !BuildSettings.bForceNoAlphaChannel;
|
|
|
|
GetOodleCompressParameters(&CompressedPixelFormat,&RDOLambda,&EffortLevel, &bLocalDebugColor,&LocalRDOUniversalTiling,BuildSettings,bHasAlpha);
|
|
|
|
FCbWriter Writer;
|
|
Writer.BeginObject("TextureFormatOodleSettings");
|
|
|
|
if ((BuildSettings.TextureFormatName == GTextureFormatNameDXT3) ||
|
|
(BuildSettings.TextureFormatName == GTextureFormatNameDXT5) ||
|
|
(BuildSettings.TextureFormatName == GTextureFormatNameDXT5n) ||
|
|
(BuildSettings.TextureFormatName == GTextureFormatNameAutoDXT) )
|
|
{
|
|
Writer.AddBool("bForceAllBC23ToBC7", bForceAllBC23ToBC7);
|
|
}
|
|
Writer.AddInteger("RDOLambda", RDOLambda);
|
|
Writer.AddInteger("EffortLevel", EffortLevel);
|
|
Writer.AddBool("bDebugColor", bLocalDebugColor);
|
|
|
|
// Don't write if default to maintain compat with any outstanding texture
|
|
// build workers.
|
|
if (LocalRDOUniversalTiling != OodleTex_RDO_UniversalTiling_Disable)
|
|
{
|
|
Writer.AddInteger("RDOUniversalTiling", LocalRDOUniversalTiling);
|
|
}
|
|
|
|
Writer.EndObject();
|
|
|
|
return Writer.Save().AsObject();
|
|
}
|
|
|
|
void GetOodleCompressParameters(EPixelFormat * OutCompressedPixelFormat,int * OutRDOLambda, OodleTex_EncodeEffortLevel * OutEffortLevel, bool * bOutDebugColor, OodleTex_RDO_UniversalTiling* OutRDOUniversalTiling, const struct FTextureBuildSettings& InBuildSettings, bool bHasAlpha) const
|
|
{
|
|
FName TextureFormatName = InBuildSettings.TextureFormatName;
|
|
|
|
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)
|
|
|
|
// if you set bForceAllBC23ToBC7, the DXT5 will change to BC7
|
|
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()
|
|
);
|
|
}
|
|
|
|
// BC7 is just always better than BC2 & BC3
|
|
// so anything that came through as BC23, force to BC7 : (AutoDXT-alpha and Normals)
|
|
// Note that we are using the value from the FormatConfigOverride if we have one, otherwise the default will be the value we have locally
|
|
if ( InBuildSettings.FormatConfigOverride.FindView("bForceAllBC23ToBC7").AsBool(bForceAllBC23ToBC7) &&
|
|
(CompressedPixelFormat == PF_DXT3 || CompressedPixelFormat == PF_DXT5 ) )
|
|
{
|
|
CompressedPixelFormat = PF_BC7;
|
|
}
|
|
|
|
*OutCompressedPixelFormat = CompressedPixelFormat;
|
|
|
|
if (InBuildSettings.FormatConfigOverride)
|
|
{
|
|
// If we have an explicit format config, then use it directly
|
|
FCbFieldView FieldView;
|
|
|
|
// RDOUniversalTiling is only set if not default, so fall back to defaults if it doesn't exist.
|
|
// Note that in this case, we're a texture build worker, so our defaults are not
|
|
// changed by GConfig.
|
|
*OutRDOUniversalTiling = (OodleTex_RDO_UniversalTiling)InBuildSettings.FormatConfigOverride.FindView("RDOUniversalTiling").AsUInt32(RDOUniversalTiling);
|
|
|
|
FieldView = InBuildSettings.FormatConfigOverride.FindView("RDOLambda");
|
|
checkf(FieldView.HasValue(), TEXT("Missing RDOLambda key from FormatConfigOverride"));
|
|
*OutRDOLambda = FieldView.AsInt32();
|
|
checkf(!FieldView.HasError(), TEXT("Failed to parse RDOLambda value from FormatConfigOverride"));
|
|
|
|
FieldView = InBuildSettings.FormatConfigOverride.FindView("EffortLevel");
|
|
checkf(FieldView.HasValue(), TEXT("Missing EffortLevel key from FormatConfigOverride"));
|
|
*OutEffortLevel = (OodleTex_EncodeEffortLevel)FieldView.AsUInt32();
|
|
checkf(!FieldView.HasError(), TEXT("Failed to parse EffortLevel value from FormatConfigOverride"));
|
|
|
|
FieldView = InBuildSettings.FormatConfigOverride.FindView("bDebugColor");
|
|
checkf(FieldView.HasValue(), TEXT("Missing bDebugColor key from FormatConfigOverride"));
|
|
*bOutDebugColor = FieldView.AsBool();
|
|
checkf(!FieldView.HasError(), TEXT("Failed to parse bDebugColor value from FormatConfigOverride"));
|
|
|
|
return;
|
|
}
|
|
|
|
*bOutDebugColor = bDebugColor;
|
|
|
|
int RDOLambda = -1;
|
|
|
|
// LossyCompressionAmount for per-Texture override
|
|
// also inherits from LODGroup if not set per-Texture
|
|
int32 LossyCompressionAmount = InBuildSettings.LossyCompressionAmount;
|
|
|
|
switch (LossyCompressionAmount)
|
|
{
|
|
default:
|
|
case TLCA_Default: break; // "Default"
|
|
case TLCA_None: RDOLambda = 0; break; // "No lossy compression"
|
|
case TLCA_Lowest: RDOLambda = 5; break; // "Lowest (Best Image quality, largest filesize)"
|
|
case TLCA_Low: RDOLambda = 15; break; // "Low"
|
|
case TLCA_Medium: RDOLambda = 30; break; // "Medium"
|
|
case TLCA_High: RDOLambda = 40; break; // "High"
|
|
case TLCA_Highest: RDOLambda = 60; break; // "Highest (Worst Image quality, smallest filesize)"
|
|
}
|
|
|
|
if ( RDOLambda == -1 )
|
|
{
|
|
// not set
|
|
// get global default from config
|
|
RDOLambda = DefaultRDOLambda;
|
|
}
|
|
|
|
if ( RDOLambda > 0 && GlobalLambdaMultiplier != 1.f )
|
|
{
|
|
RDOLambda = (int)( GlobalLambdaMultiplier * RDOLambda + 0.5f );
|
|
// don't let it change to 0 :
|
|
if ( RDOLambda <= 0 )
|
|
{
|
|
RDOLambda = 1;
|
|
}
|
|
}
|
|
|
|
RDOLambda = FMath::Clamp(RDOLambda,0,100);
|
|
|
|
// ini option to force non-RDO encoding :
|
|
bool bForceRDOOff = InBuildSettings.bHasEditorOnlyData ? bForceRDOOff_Editor : bForceRDOOff_NoEditor;
|
|
if ( bForceRDOOff )
|
|
{
|
|
RDOLambda = 0;
|
|
}
|
|
|
|
// "Normal" is medium quality/speed
|
|
OodleTex_EncodeEffortLevel EffortLevel = InBuildSettings.bHasEditorOnlyData ? CompressEffortLevel_Editor : CompressEffortLevel_NoEditor;
|
|
// 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)
|
|
|
|
*OutRDOLambda = RDOLambda;
|
|
*OutEffortLevel = EffortLevel;
|
|
*OutRDOUniversalTiling = RDOUniversalTiling;
|
|
}
|
|
|
|
private:
|
|
// the sense of these bools is set so that default behavior = all false
|
|
bool bForceAllBC23ToBC7; // change BC2 & 3 (aka DXT3 and DXT5) to BC7
|
|
bool bForceRDOOff_NoEditor; // use Oodle Texture but without RDO ; for debugging/testing , use LossyCompresionAmount to do this per-Texture
|
|
bool bForceRDOOff_Editor; // bForceRDOOff in Editor
|
|
OodleTex_EncodeEffortLevel CompressEffortLevel_NoEditor; // how much time to spend encoding to get higher quality
|
|
OodleTex_EncodeEffortLevel CompressEffortLevel_Editor; // CompressEffortLevel in Editor
|
|
OodleTex_RDO_UniversalTiling RDOUniversalTiling; // whether to use universal tiling, and at what block size.
|
|
bool bDebugColor; // color textures by their BCN, for data discovery
|
|
// if no lambda is set on Texture or lodgroup, fall through to this global default :
|
|
int DefaultRDOLambda;
|
|
// 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;
|
|
|
|
FTextureFormatOodle()
|
|
{
|
|
}
|
|
|
|
|
|
virtual ~FTextureFormatOodle()
|
|
{
|
|
}
|
|
|
|
virtual bool AllowParallelBuild() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
virtual bool UsesTaskGraph() const override
|
|
{
|
|
// @todo the UsesTaskGraph function should go away entirely from ITextureFormat
|
|
// it's only being used by VirtualTextureDataBuilder
|
|
// it's none of his business
|
|
// if that's a deadlock, there should be a better solution
|
|
// like let me ask if I'm being called from a ParallelFor
|
|
return true;
|
|
}
|
|
|
|
virtual FCbObject ExportGlobalFormatConfig(const FTextureBuildSettings& BuildSettings) const override
|
|
{
|
|
return GlobalFormatConfig.ExportToCb(BuildSettings);
|
|
}
|
|
|
|
void Init()
|
|
{
|
|
// this is done at Singleton init time, the first time GetTextureFormat() is called
|
|
GlobalFormatConfig.ImportFromConfigCache();
|
|
}
|
|
|
|
|
|
// increment this to invalidate Derived Data Cache to recompress everything
|
|
#define DDC_OODLE_TEXTURE_VERSION 12
|
|
|
|
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 GetDerivedDataKeyString(const FTextureBuildSettings& InBuildSettings) 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;
|
|
EPixelFormat CompressedPixelFormat;
|
|
bool bDebugColor;
|
|
|
|
// @todo Oodle this is not quite the same "bHasAlpha" that Compress will see
|
|
// bHasAlpha is used for AutoDXT -> DXT1/5
|
|
// we do have Texture.bForceNoAlphaChannel/CompressionNoAlpha but that's not quite what we want
|
|
// do go ahead and read bForceNoAlphaChannel/CompressionNoAlpha so that we invalidate DDC when that changes
|
|
bool bHasAlpha = !InBuildSettings.bForceNoAlphaChannel;
|
|
|
|
GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat,&RDOLambda,&EffortLevel,&bDebugColor,&RDOUniversalTiling,InBuildSettings,bHasAlpha);
|
|
|
|
// store the actual lambda in DDC key (rather than "LossyCompressionAmount")
|
|
// that way any changes in how LossyCompressionAmount maps to lambda get rebuilt
|
|
|
|
int icpf = (int)CompressedPixelFormat;
|
|
|
|
check(RDOLambda<256);
|
|
if (bDebugColor)
|
|
{
|
|
RDOLambda = 256;
|
|
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);
|
|
}
|
|
|
|
#ifdef DO_FORCE_UNIQUE_DDC_KEY_PER_BUILD
|
|
DDCString += TEXT(__DATE__);
|
|
DDCString += TEXT(__TIME__);
|
|
#endif
|
|
|
|
return DDCString;
|
|
}
|
|
|
|
virtual void GetSupportedFormats(TArray<FName>& OutFormats) const override
|
|
{
|
|
OutFormats.Append(GSupportedTextureFormatNames, sizeof(GSupportedTextureFormatNames)/sizeof(GSupportedTextureFormatNames[0]) );
|
|
}
|
|
|
|
virtual FTextureFormatCompressorCaps GetFormatCapabilities() const override
|
|
{
|
|
return FTextureFormatCompressorCaps(); // Default capabilities.
|
|
}
|
|
|
|
virtual EPixelFormat GetPixelFormatForImage(const FTextureBuildSettings& InBuildSettings, const struct FImage& Image, bool bHasAlpha) const override
|
|
{
|
|
int RDOLambda;
|
|
OodleTex_EncodeEffortLevel EffortLevel;
|
|
OodleTex_RDO_UniversalTiling RDOUniversalTiling;
|
|
EPixelFormat CompressedPixelFormat;
|
|
bool bDebugColor;
|
|
|
|
GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat,&RDOLambda,&EffortLevel,&bDebugColor,&RDOUniversalTiling,InBuildSettings,bHasAlpha);
|
|
return CompressedPixelFormat;
|
|
}
|
|
|
|
virtual bool CompressImage(const FImage& InImage, const FTextureBuildSettings& InBuildSettings, const bool bInHasAlpha, FCompressedImage2D& OutImage) const override
|
|
{
|
|
check(InImage.SizeX > 0);
|
|
check(InImage.SizeY > 0);
|
|
check(InImage.NumSlices > 0);
|
|
|
|
// InImage always comes in as F32 in linear light
|
|
// (Unreal has just made mips in that format)
|
|
// we are run simultaneously on all mips using the GLargeThreadPool
|
|
|
|
// 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;
|
|
EPixelFormat CompressedPixelFormat;
|
|
bool bDebugColor;
|
|
GlobalFormatConfig.GetOodleCompressParameters(&CompressedPixelFormat,&RDOLambda,&EffortLevel,&bDebugColor,&RDOUniversalTiling,InBuildSettings,bHasAlpha);
|
|
|
|
OodleTex_BC OodleBCN = OodleTex_BC_Invalid;
|
|
if ( CompressedPixelFormat == PF_DXT1 ) { OodleBCN = OodleTex_BC1_WithTransparency; bHasAlpha = false; }
|
|
else if ( CompressedPixelFormat == PF_DXT3 ) { OodleBCN = OodleTex_BC2; }
|
|
else if ( CompressedPixelFormat == PF_DXT5 ) { OodleBCN = OodleTex_BC3; }
|
|
else if ( CompressedPixelFormat == PF_BC4 ) { OodleBCN = OodleTex_BC4U; }
|
|
else if ( CompressedPixelFormat == PF_BC5 ) { OodleBCN = OodleTex_BC5U; }
|
|
else if ( CompressedPixelFormat == PF_BC6H ) { OodleBCN = OodleTex_BC6U; }
|
|
else if ( CompressedPixelFormat == PF_BC7 ) { OodleBCN = OodleTex_BC7RGBA; }
|
|
else
|
|
{
|
|
UE_LOG(LogTextureFormatOodle,Fatal,
|
|
TEXT("Unsupported CompressedPixelFormat for compression: %d"),
|
|
(int)CompressedPixelFormat
|
|
);
|
|
}
|
|
|
|
FName TextureFormatName = InBuildSettings.TextureFormatName;
|
|
|
|
// 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("%s encode %i x %i x %i to format %s (Oodle %s) lambda=%i effort=%i "), \
|
|
RDOLambda ? TEXT("RDO") : TEXT("non-RDO"), InImage.SizeX, InImage.SizeY, InImage.NumSlices,
|
|
*TextureFormatName.ToString(), *FString(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
|
|
// because Unreal only does Gamma correction on the 8 bit conversion
|
|
// (this loses precision for BC4,5 which would like 16 bit input)
|
|
|
|
EGammaSpace Gamma = InBuildSettings.GetGammaSpace();
|
|
// note in unreal if Gamma == Pow22 due to legacy Gamma,
|
|
// we still want to encode to sRGB
|
|
// (CopyTo does that even without this change, but let's make it explicit)
|
|
if ( Gamma == EGammaSpace::Pow22 ) Gamma = EGammaSpace::sRGB;
|
|
|
|
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(OodleTex_BC_GetName(OodleBCN)) );
|
|
}
|
|
|
|
ERawImageFormat::Type ImageFormat;
|
|
OodleTex_PixelFormat OodlePF;
|
|
|
|
if (OodleBCN == OodleTex_BC6U)
|
|
{
|
|
ImageFormat = ERawImageFormat::RGBA32F;
|
|
OodlePF = OodleTex_PixelFormat_4_F32_RGBA;
|
|
// 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;
|
|
}
|
|
else if ((OodleBCN == OodleTex_BC4U || OodleBCN == OodleTex_BC5U) &&
|
|
Gamma == EGammaSpace::Linear &&
|
|
!bDebugColor)
|
|
{
|
|
// for BC4/5 use 16-bit :
|
|
// BC4/5 should always have linear gamma
|
|
// @todo we only need 1 or 2 channel 16-bit, not all 4; use our own converter
|
|
// or just let our encoder take F32 input?
|
|
ImageFormat = ERawImageFormat::RGBA16;
|
|
OodlePF = OodleTex_PixelFormat_4_U16;
|
|
}
|
|
else
|
|
{
|
|
ImageFormat = ERawImageFormat::BGRA8;
|
|
// if requested format was DXT1
|
|
// Unreal assumes that will not encode any alpha channel in the source
|
|
// (Unreal's "compress without alpha" just selects DXT1)
|
|
// the legacy NVTT behavior for DXT1 was to always encode opaque pixels
|
|
// for DXT1 we use BC1_WithTransparency which will preserve the input A transparency bit
|
|
// so we need to force the A's to be 255 coming into Oodle
|
|
// so for DXT1 we force bHasAlpha = false
|
|
// force Oodle to ignore input alpha :
|
|
OodlePF = bHasAlpha ? OodleTex_PixelFormat_4_U8_BGRA : OodleTex_PixelFormat_4_U8_BGRx;
|
|
}
|
|
|
|
bool bNeedsImageCopy = ImageFormat != InImage.Format ||
|
|
Gamma != InImage.GammaSpace ||
|
|
(CompressedPixelFormat == PF_DXT5 && TextureFormatName == GTextureFormatNameDXT5n) ||
|
|
bDebugColor;
|
|
FImage ImageCopy;
|
|
if (bNeedsImageCopy)
|
|
{
|
|
InImage.CopyTo(ImageCopy, ImageFormat, Gamma);
|
|
}
|
|
const FImage& Image = bNeedsImageCopy ? ImageCopy : InImage;
|
|
|
|
// verify OodlePF matches Image :
|
|
check( Image.GetBytesPerPixel() == OodleTex_PixelFormat_BytesPerPixel(OodlePF) );
|
|
|
|
OodleTex_Surface InSurf = { 0 };
|
|
InSurf.width = Image.SizeX;
|
|
InSurf.height = Image.SizeY;
|
|
InSurf.pixels = 0;
|
|
InSurf.rowStrideBytes = Image.GetBytesPerPixel() * Image.SizeX;
|
|
|
|
SSIZE_T InBytesPerSlice = InSurf.rowStrideBytes * 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
|
|
// lets you visually identify BCN textures in the Editor or game
|
|
|
|
// 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);
|
|
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;
|
|
}
|
|
|
|
for(uint8 * ptr = ImageBasePtr; ptr < (ImageBasePtr + InBytesTotal); ptr += 4)
|
|
{
|
|
*((uint32 *)ptr) = DebugColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
int BytesPerBlock = 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;
|
|
OutImage.SizeX = NumBlocksX*4;
|
|
OutImage.SizeY = NumBlocksY*4;
|
|
// note: cubes come in as 6 slices and go out as 1
|
|
OutImage.SizeZ = (InBuildSettings.bVolume || InBuildSettings.bTextureArray) ? Image.NumSlices : 1;
|
|
OutImage.RawData.AddUninitialized(OutBytesTotal);
|
|
|
|
|
|
uint8 * OutBlocksBasePtr = (uint8 *) &OutImage.RawData[0];
|
|
|
|
FImageDumper ImageDumper;
|
|
bool bImageDump = false;
|
|
if (GlobalFormatConfig.GetLocalDebugConfig().bDebugDump && !bDebugColor)
|
|
{
|
|
if (ImageDumper.Initialize(ImageFormat))
|
|
{
|
|
bImageDump = true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle Texture debug dump initialization failed!"));
|
|
}
|
|
}
|
|
|
|
int CurJobifyNumThreads = OodleJobifyNumThreads;
|
|
void* CurJobifyUserPointer = OodleJobifyUserPointer;
|
|
|
|
|
|
// @todo check its safe to do TaskGraph waits from inside TaskGraph threads?
|
|
// see also VirtualTextureDataBuilder.cpp UsesTaskGraph
|
|
//const bool bVTDisableInternalThreading = false; // false = DO use internal threads on VT
|
|
const bool bVTDisableInternalThreading = true; // true = DO NOT use internal threads on VT
|
|
bool bIsVT = InBuildSettings.bVirtualStreamable;
|
|
|
|
if (bIsVT && bVTDisableInternalThreading)
|
|
{
|
|
// VT runs its tiles in a ParallelFor on the TaskGraph
|
|
// if we use TaskGraph internally there's a chance of deadlock (?)
|
|
// disable our own internal threading for VT tiles :
|
|
CurJobifyNumThreads = OODLETEX_JOBS_DISABLE;
|
|
CurJobifyUserPointer = nullptr;
|
|
}
|
|
|
|
// encode each slice
|
|
// @todo Oodle alternatively could do [Image.NumSlices] array of OodleTex_Surface
|
|
// and call OodleTex_Encode with the array
|
|
// would be slightly better for parallelism with multi-slice images & cube maps
|
|
// that's a rare case so don't bother for now
|
|
// (the main parallelism is from running many mips or VT tiles at once which is done by our caller)
|
|
bool bCompressionSucceeded = true;
|
|
for (int Slice = 0; Slice < Image.NumSlices; ++Slice)
|
|
{
|
|
InSurf.pixels = ImageBasePtr + Slice * InBytesPerSlice;
|
|
uint8 * OutSlicePtr = OutBlocksBasePtr + Slice * OutBytesPerSlice;
|
|
|
|
if (bImageDump && !ImageDumper.DumpImage(InSurf.pixels, (int64)Image.GetBytesPerPixel() * Image.SizeX * Image.SizeY, Image.SizeX, Image.SizeY, Slice, RDOLambda, OodleBCN))
|
|
{
|
|
UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle Texture debug dump failed!"));
|
|
}
|
|
|
|
OodleTex_RDO_Options OodleOptions = { };
|
|
OodleOptions.effort = EffortLevel;
|
|
OodleOptions.metric = OodleTex_RDO_ErrorMetric_Default;
|
|
OodleOptions.bcn_flags = OodleTex_BCNFlags_None;
|
|
OodleOptions.universal_tiling = RDOUniversalTiling;
|
|
|
|
// if RDOLambda == 0, does non-RDO encode :
|
|
OodleTex_Err OodleErr = OodleTex_EncodeBCN_RDO_Ex(OodleBCN, OutSlicePtr, NumBlocksPerSlice,
|
|
&InSurf, 1, OodlePF, NULL, RDOLambda,
|
|
&OodleOptions, CurJobifyNumThreads, CurJobifyUserPointer);
|
|
|
|
if (OodleErr != OodleTex_Err_OK)
|
|
{
|
|
const char * OodleErrStr = OodleTex_Err_GetName(OodleErr);
|
|
UE_LOG(LogTextureFormatOodle, Display, TEXT("Oodle Texture encode failed!? %s"), OodleErrStr );
|
|
bCompressionSucceeded = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bCompressionSucceeded;
|
|
}
|
|
};
|
|
|
|
//===============================================================
|
|
|
|
static ITextureFormat* Singleton = NULL;
|
|
|
|
|
|
// TFO_ plugins to Oodle to run Oodle system services in Unreal
|
|
// @todo Oodle : factor this out and share for Core & Net some day
|
|
|
|
// global map of TaskGraph references to uint64 for Oodle Jobify system
|
|
// protected by TaskIdMapLock
|
|
static uint8 PadToCacheLine1[64];
|
|
static FCriticalSection TaskIdMapLock;
|
|
// would be more efficient to split task ids into bins to reduce contention
|
|
static uint64 NextTaskId = 1;
|
|
static TMap<uint64, FGraphEventRef> TaskIdMap;
|
|
static uint8 PadToCacheLine2[64];
|
|
|
|
static OO_U64 OODLE_CALLBACK TFO_RunJob(t_fp_Oodle_Job* JobFunction, void* JobData, OO_U64* Dependencies, int NumDependencies, void* UserPtr)
|
|
{
|
|
FGraphEventArray Prerequisites;
|
|
if ( NumDependencies > 0 )
|
|
{
|
|
// map uint64 dependencies to TaskGraph refs
|
|
Prerequisites.Reserve(NumDependencies);
|
|
|
|
FScopeLock Lock(&TaskIdMapLock);
|
|
for (int DependencyIndex = 0; DependencyIndex < NumDependencies; DependencyIndex++)
|
|
{
|
|
uint64 Id = Dependencies[DependencyIndex];
|
|
FGraphEventRef Task = TaskIdMap[Id];
|
|
// operator [] does a check that Task was found
|
|
Prerequisites.Add(Task);
|
|
}
|
|
}
|
|
|
|
// don't hold TaskIdMapLock while dispatching task
|
|
|
|
// Use AnyBackgroundThreadNormalTask 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?
|
|
FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady(
|
|
[JobFunction, JobData]()
|
|
{
|
|
JobFunction(JobData);
|
|
}, TStatId(), &Prerequisites, IsInGameThread() ? ENamedThreads::AnyThread : ENamedThreads::AnyBackgroundThreadNormalTask);
|
|
|
|
// scope lock for NextTaskId and TaskIdMap
|
|
TaskIdMapLock.Lock();
|
|
uint64 Id = NextTaskId++;
|
|
TaskIdMap.Add(Id, MoveTemp(Task));
|
|
TaskIdMapLock.Unlock();
|
|
|
|
return Id;
|
|
}
|
|
|
|
static void OODLE_CALLBACK TFO_WaitJob(OO_U64 JobHandle, void* UserPtr)
|
|
{
|
|
TaskIdMapLock.Lock();
|
|
FGraphEventRef Task = TaskIdMap[JobHandle];
|
|
// TMap operator [] checks that value is found
|
|
// can remove immediately (task may still be running)
|
|
// because once WaitJob is called this handle can never be referred to by calling code
|
|
TaskIdMap.Remove(JobHandle);
|
|
TaskIdMapLock.Unlock();
|
|
|
|
// don't hold TaskIdMapLock while waiting
|
|
FTaskGraphInterface::Get().WaitUntilTaskCompletes(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)
|
|
{
|
|
void * Ret = FMemory::Malloc(Bytes, Alignment);
|
|
check( Ret != nullptr );
|
|
return Ret;
|
|
}
|
|
|
|
static void OODLE_CALLBACK TFO_OodleFree(void* ptr)
|
|
{
|
|
FMemory::Free(ptr);
|
|
}
|
|
|
|
static void TFO_InstallPlugins()
|
|
{
|
|
// 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
|
|
const TCHAR* IniSection = TEXT("TextureFormatOodleSettings");
|
|
bool UseOodleJobify = false;
|
|
GConfig->GetBool(IniSection, TEXT("UseOodleExampleJobify"), UseOodleJobify, GEngineIni);
|
|
|
|
if (UseOodleJobify)
|
|
{
|
|
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;
|
|
OodleTex_Plugins_SetJobSystemAndCount(example_jobify_run_job_fptr, example_jobify_wait_job_fptr, example_jobify_target_parallelism);
|
|
}
|
|
else
|
|
{
|
|
OodleJobifyUserPointer = (void *)1; //anything non-null
|
|
OodleJobifyNumThreads = FTaskGraphInterface::Get().GetNumWorkerThreads();
|
|
|
|
|
|
OodleTex_Plugins_SetJobSystemAndCount(TFO_RunJob, TFO_WaitJob, OodleJobifyNumThreads);
|
|
}
|
|
|
|
OodleTex_Plugins_SetAssertion(TFO_OodleAssert);
|
|
OodleTex_Plugins_SetPrintf(TFO_OodleLog);
|
|
OodleTex_Plugins_SetAllocators(TFO_OodleMallocAligned, TFO_OodleFree);
|
|
}
|
|
|
|
class FOodleTextureBuildFunction final : public FTextureBuildFunction
|
|
{
|
|
FStringView GetName() const final { return TEXT("OodleTexture"); }
|
|
FGuid GetVersion() const final { return FGuid(TEXT("e6b8884f-923a-44a1-8da1-298fb48865b2")); }
|
|
};
|
|
|
|
class FTextureFormatOodleModule : public ITextureFormatModule
|
|
{
|
|
public:
|
|
FTextureFormatOodleModule() { }
|
|
virtual ~FTextureFormatOodleModule()
|
|
{
|
|
ITextureFormat * p = Singleton;
|
|
Singleton = NULL;
|
|
if ( p )
|
|
delete p;
|
|
}
|
|
|
|
virtual void StartupModule() override
|
|
{
|
|
}
|
|
|
|
virtual ITextureFormat* GetTextureFormat()
|
|
{
|
|
// this is called twice
|
|
|
|
if (!Singleton) // not thread safe
|
|
{
|
|
TFO_InstallPlugins();
|
|
|
|
FTextureFormatOodle * ptr = new FTextureFormatOodle();
|
|
ptr->Init();
|
|
Singleton = ptr;
|
|
}
|
|
return Singleton;
|
|
}
|
|
|
|
static UE::DerivedData::TBuildFunctionFactory<FOodleTextureBuildFunction> BuildFunctionFactory;
|
|
};
|
|
|
|
UE::DerivedData::TBuildFunctionFactory<FOodleTextureBuildFunction> FTextureFormatOodleModule::BuildFunctionFactory;
|
|
|
|
IMPLEMENT_MODULE(FTextureFormatOodleModule, TextureFormatOodle);
|
|
|