You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Expand texture build workers to all current platform specific texture formats, or added build functions to the base build worker. Workers are buildable, but not discoverable yet as discovery will be refactored soon to use Target Receipts. Reduce boilerplate involved in setup of build worker. #rb devin.doucette #ROBOMERGE-SOURCE: CL 16853856 in //UE5/Main/... #ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v836-16769935) [CL 16853877 by zousar shaker in ue5-release-engine-test branch]
941 lines
31 KiB
C++
941 lines
31 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Containers/IndirectArray.h"
|
|
#include "Stats/Stats.h"
|
|
#include "Async/AsyncWork.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "ImageCore.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "Interfaces/ITextureFormat.h"
|
|
#include "Interfaces/ITextureFormatModule.h"
|
|
#include "TextureCompressorModule.h"
|
|
#include "PixelFormat.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "Serialization/CompactBinary.h"
|
|
#include "Serialization/CompactBinaryWriter.h"
|
|
#include "TextureBuildFunction.h"
|
|
#include "DerivedDataBuildFunctionFactory.h"
|
|
|
|
#include "ispc_texcomp.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogTextureFormatIntelISPCTexComp, Log, All);
|
|
|
|
class FIntelISPCTexCompTextureBuildFunction final : public FTextureBuildFunction
|
|
{
|
|
FStringView GetName() const final { return TEXT("IntelISPCTexCompTexture"); }
|
|
FGuid GetVersion() const final { return FGuid(TEXT("19d413ad-f529-4687-902a-3b71919cfd72")); }
|
|
};
|
|
|
|
// increment this if you change anything that will affect compression in this file, including FORCED_NORMAL_MAP_COMPRESSION_SIZE_VALUE
|
|
#define BASE_ISPC_DX11_FORMAT_VERSION 4
|
|
|
|
// For debugging intermediate image results by saving them out as files.
|
|
#define DEBUG_SAVE_INTERMEDIATE_IMAGES 0
|
|
|
|
/**
|
|
* Macro trickery for supported format names.
|
|
*/
|
|
#define ENUM_SUPPORTED_FORMATS(op) \
|
|
op(BC6H) \
|
|
op(BC7)
|
|
|
|
#define DECL_FORMAT_NAME(FormatName) static FName GTextureFormatName##FormatName = FName(TEXT(#FormatName));
|
|
ENUM_SUPPORTED_FORMATS(DECL_FORMAT_NAME);
|
|
#undef DECL_FORMAT_NAME
|
|
|
|
#define DECL_FORMAT_NAME_ENTRY(FormatName) GTextureFormatName##FormatName ,
|
|
static FName GSupportedTextureFormatNames[] =
|
|
{
|
|
ENUM_SUPPORTED_FORMATS(DECL_FORMAT_NAME_ENTRY)
|
|
};
|
|
#undef DECL_FORMAT_NAME_ENTRY
|
|
#undef ENUM_SUPPORTED_FORMATS
|
|
|
|
#define ENUM_ASTC_FORMATS(op) \
|
|
op(ASTC_RGB) \
|
|
op(ASTC_RGBA) \
|
|
op(ASTC_RGBAuto) \
|
|
op(ASTC_NormalAG) \
|
|
op(ASTC_NormalRG)
|
|
|
|
#define DECL_FORMAT_NAME(FormatName) static FName GTextureFormatName##FormatName = FName(TEXT(#FormatName));
|
|
ENUM_ASTC_FORMATS(DECL_FORMAT_NAME);
|
|
#undef DECL_FORMAT_NAME
|
|
#undef ENUM_ASTC_FORMATS
|
|
|
|
// BC6H, BC7, ASTC all have 16-byte block size
|
|
#define BLOCK_SIZE_IN_BYTES 16
|
|
|
|
|
|
// Bitmap compression types.
|
|
enum EBitmapCompression
|
|
{
|
|
BCBI_RGB = 0,
|
|
BCBI_RLE8 = 1,
|
|
BCBI_RLE4 = 2,
|
|
BCBI_BITFIELDS = 3,
|
|
};
|
|
|
|
// .BMP file header.
|
|
#pragma pack(push,1)
|
|
struct FBitmapFileHeader
|
|
{
|
|
uint16 bfType;
|
|
uint32 bfSize;
|
|
uint16 bfReserved1;
|
|
uint16 bfReserved2;
|
|
uint32 bfOffBits;
|
|
friend FArchive& operator<<(FArchive& Ar, FBitmapFileHeader& H)
|
|
{
|
|
Ar << H.bfType << H.bfSize << H.bfReserved1 << H.bfReserved2 << H.bfOffBits;
|
|
return Ar;
|
|
}
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
// .BMP subheader.
|
|
#pragma pack(push,1)
|
|
struct FBitmapInfoHeader
|
|
{
|
|
uint32 biSize;
|
|
uint32 biWidth;
|
|
int32 biHeight;
|
|
uint16 biPlanes;
|
|
uint16 biBitCount;
|
|
uint32 biCompression;
|
|
uint32 biSizeImage;
|
|
uint32 biXPelsPerMeter;
|
|
uint32 biYPelsPerMeter;
|
|
uint32 biClrUsed;
|
|
uint32 biClrImportant;
|
|
friend FArchive& operator<<(FArchive& Ar, FBitmapInfoHeader& H)
|
|
{
|
|
Ar << H.biSize << H.biWidth << H.biHeight;
|
|
Ar << H.biPlanes << H.biBitCount;
|
|
Ar << H.biCompression << H.biSizeImage;
|
|
Ar << H.biXPelsPerMeter << H.biYPelsPerMeter;
|
|
Ar << H.biClrUsed << H.biClrImportant;
|
|
return Ar;
|
|
}
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
|
|
void SaveImageAsBMP( FArchive& Ar, const uint8* RawData, int SourceBytesPerPixel, int SizeX, int SizeY )
|
|
{
|
|
FBitmapFileHeader bmf;
|
|
FBitmapInfoHeader bmhdr;
|
|
|
|
// File header.
|
|
bmf.bfType = 'B' + (256 * (int32)'M');
|
|
bmf.bfReserved1 = 0;
|
|
bmf.bfReserved2 = 0;
|
|
int32 biSizeImage = SizeX * SizeY * 3;
|
|
bmf.bfOffBits = sizeof(FBitmapFileHeader) + sizeof(FBitmapInfoHeader);
|
|
bmhdr.biBitCount = 24;
|
|
|
|
bmf.bfSize = bmf.bfOffBits + biSizeImage;
|
|
Ar << bmf;
|
|
|
|
// Info header.
|
|
bmhdr.biSize = sizeof(FBitmapInfoHeader);
|
|
bmhdr.biWidth = SizeX;
|
|
bmhdr.biHeight = SizeY;
|
|
bmhdr.biPlanes = 1;
|
|
bmhdr.biCompression = BCBI_RGB;
|
|
bmhdr.biSizeImage = biSizeImage;
|
|
bmhdr.biXPelsPerMeter = 0;
|
|
bmhdr.biYPelsPerMeter = 0;
|
|
bmhdr.biClrUsed = 0;
|
|
bmhdr.biClrImportant = 0;
|
|
Ar << bmhdr;
|
|
|
|
bool bIsRGBA16 = (SourceBytesPerPixel == 8);
|
|
|
|
//NOTE: Each row must be 4-byte aligned in a BMP.
|
|
int PaddingX = Align(SizeX * 3, 4) - SizeX * 3;
|
|
|
|
// Upside-down scanlines.
|
|
for (int32 i = SizeY - 1; i >= 0; i--)
|
|
{
|
|
const uint8* ScreenPtr = &RawData[i*SizeX*SourceBytesPerPixel];
|
|
for (int32 j = SizeX; j > 0; j--)
|
|
{
|
|
uint8 R, G, B;
|
|
if (bIsRGBA16)
|
|
{
|
|
R = ScreenPtr[1];
|
|
G = ScreenPtr[3];
|
|
B = ScreenPtr[5];
|
|
ScreenPtr += 8;
|
|
}
|
|
else
|
|
{
|
|
R = ScreenPtr[0];
|
|
G = ScreenPtr[1];
|
|
B = ScreenPtr[2];
|
|
ScreenPtr += 4;
|
|
}
|
|
Ar << R;
|
|
Ar << G;
|
|
Ar << B;
|
|
}
|
|
for (int32 j = 0; j < PaddingX; ++j)
|
|
{
|
|
int8 PadByte = 0;
|
|
Ar << PadByte;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define MAGIC_FILE_CONSTANT 0x5CA1AB13
|
|
|
|
// little endian
|
|
#pragma pack(push,1)
|
|
struct astc_header
|
|
{
|
|
uint8_t magic[4];
|
|
uint8_t blockdim_x;
|
|
uint8_t blockdim_y;
|
|
uint8_t blockdim_z;
|
|
uint8_t xsize[3];
|
|
uint8_t ysize[3]; // x-size, y-size and z-size are given in texels;
|
|
uint8_t zsize[3]; // block count is inferred
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
void SaveImageAsASTC(FArchive& Ar, uint8* RawData, int SizeX, int SizeY, int block_width, int block_height)
|
|
{
|
|
astc_header file_header;
|
|
|
|
uint32_t magic = MAGIC_FILE_CONSTANT;
|
|
FMemory::Memcpy(file_header.magic, &magic, 4);
|
|
file_header.blockdim_x = block_width;
|
|
file_header.blockdim_y = block_height;
|
|
file_header.blockdim_z = 1;
|
|
|
|
int32 xsize = SizeX;
|
|
int32 ysize = SizeY;
|
|
int32 zsize = 1;
|
|
|
|
FMemory::Memcpy(file_header.xsize, &xsize, 3);
|
|
FMemory::Memcpy(file_header.ysize, &ysize, 3);
|
|
FMemory::Memcpy(file_header.zsize, &zsize, 3);
|
|
|
|
Ar.Serialize(&file_header, sizeof(file_header));
|
|
|
|
size_t height_in_blocks = (SizeY + block_height - 1) / block_height;
|
|
size_t width_in_blocks = (SizeX + block_width - 1) / block_width;
|
|
int stride = width_in_blocks * BLOCK_SIZE_IN_BYTES;
|
|
Ar.Serialize(RawData, height_in_blocks * stride);
|
|
}
|
|
|
|
struct FMultithreadSettings
|
|
{
|
|
int iScansPerTask;
|
|
int iNumTasks;
|
|
};
|
|
|
|
template <typename EncoderSettingsType>
|
|
struct FMultithreadedCompression
|
|
{
|
|
typedef void(*CompressFunction)(EncoderSettingsType* pEncSettings, FImage* pInImage, FCompressedImage2D* pOutImage, int yStart, int yEnd, int SliceIndex);
|
|
|
|
static void Compress(FMultithreadSettings &MultithreadSettings, EncoderSettingsType &EncoderSettings, FImage &Image, FCompressedImage2D &OutCompressedImage, CompressFunction FunctionCallback, bool bUseTasks)
|
|
{
|
|
if (bUseTasks)
|
|
{
|
|
class FIntelCompressWorker
|
|
{
|
|
public:
|
|
FIntelCompressWorker(EncoderSettingsType* pEncSettings, FImage* pInImage, FCompressedImage2D* pOutImage, int yStart, int yEnd, int SliceIndex, CompressFunction InFunctionCallback)
|
|
: mpEncSettings(pEncSettings)
|
|
, mpInImage(pInImage)
|
|
, mpOutImage(pOutImage)
|
|
, mYStart(yStart)
|
|
, mYEnd(yEnd)
|
|
, mSliceIndex(SliceIndex)
|
|
, mCallback(InFunctionCallback)
|
|
{
|
|
}
|
|
|
|
void DoWork()
|
|
{
|
|
mCallback(mpEncSettings, mpInImage, mpOutImage, mYStart, mYEnd, mSliceIndex);
|
|
}
|
|
|
|
EncoderSettingsType* mpEncSettings;
|
|
FImage* mpInImage;
|
|
FCompressedImage2D* mpOutImage;
|
|
int mYStart;
|
|
int mYEnd;
|
|
int mSliceIndex;
|
|
CompressFunction mCallback;
|
|
};
|
|
|
|
// One less task because we'll do the final + non multiple of 4 inside this task
|
|
TArray<FIntelCompressWorker> CompressionTasks;
|
|
const int NumStasksPerSlice = MultithreadSettings.iNumTasks + 1;
|
|
CompressionTasks.Reserve(NumStasksPerSlice * Image.NumSlices - 1);
|
|
for (int SliceIndex = 0; SliceIndex < Image.NumSlices; ++SliceIndex)
|
|
{
|
|
for (int iTask = 0; iTask < NumStasksPerSlice; ++iTask)
|
|
{
|
|
// Create a new task unless it's the last task in the last slice (that one will run on current thread, after these threads have been started)
|
|
if (SliceIndex < (Image.NumSlices - 1) || iTask < (NumStasksPerSlice - 1))
|
|
{
|
|
CompressionTasks.Emplace(&EncoderSettings, &Image, &OutCompressedImage, iTask * MultithreadSettings.iScansPerTask, (iTask + 1) * MultithreadSettings.iScansPerTask, SliceIndex, FunctionCallback);
|
|
}
|
|
}
|
|
}
|
|
|
|
ParallelForWithPreWork(CompressionTasks.Num(), [&CompressionTasks](int32 TaskIndex)
|
|
{
|
|
CompressionTasks[TaskIndex].DoWork();
|
|
},
|
|
[&EncoderSettings, &Image, &OutCompressedImage, &MultithreadSettings, &FunctionCallback]()
|
|
{
|
|
FunctionCallback(&EncoderSettings, &Image, &OutCompressedImage, MultithreadSettings.iScansPerTask * MultithreadSettings.iNumTasks, Image.SizeY, Image.NumSlices - 1);
|
|
}, EParallelForFlags::Unbalanced);
|
|
}
|
|
else
|
|
{
|
|
for (int SliceIndex = 0; SliceIndex < Image.NumSlices; ++SliceIndex)
|
|
{
|
|
FunctionCallback(&EncoderSettings, &Image, &OutCompressedImage, 0, Image.SizeY, SliceIndex);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* BC6H Compression function
|
|
*/
|
|
static void IntelBC6HCompressScans(bc6h_enc_settings* pEncSettings, FImage* pInImage, FCompressedImage2D* pOutImage, int yStart, int yEnd, int SliceIndex)
|
|
{
|
|
check(pInImage->Format == ERawImageFormat::RGBA16F);
|
|
check((yStart % 4) == 0);
|
|
check((pInImage->SizeX % 4) == 0);
|
|
check((yStart >= 0) && (yStart <= pInImage->SizeY));
|
|
check((yEnd >= 0) && (yEnd <= pInImage->SizeY));
|
|
|
|
const int64 InStride = (int64)pInImage->SizeX * 8;
|
|
const int64 OutStride = (int64)pInImage->SizeX / 4 * BLOCK_SIZE_IN_BYTES;
|
|
const int64 InSliceSize = (int64)pInImage->SizeY * InStride;
|
|
const int64 OutSliceSize = (int64)pInImage->SizeY / 4 * OutStride;
|
|
|
|
uint8* pInTexels = reinterpret_cast<uint8*>(&pInImage->RawData[0]) + InSliceSize * SliceIndex;
|
|
uint8* pOutTexels = reinterpret_cast<uint8*>(&pOutImage->RawData[0]) + OutSliceSize * SliceIndex;
|
|
|
|
rgba_surface insurface;
|
|
insurface.ptr = pInTexels + (yStart * InStride);
|
|
insurface.width = pInImage->SizeX;
|
|
insurface.height = yEnd - yStart;
|
|
insurface.stride = pInImage->SizeX * 8;
|
|
|
|
pOutTexels += yStart / 4 * OutStride;
|
|
CompressBlocksBC6H(&insurface, pOutTexels, pEncSettings);
|
|
}
|
|
|
|
/**
|
|
* BC7 Compression function
|
|
*/
|
|
static void IntelBC7CompressScans(bc7_enc_settings* pEncSettings, FImage* pInImage, FCompressedImage2D* pOutImage, int yStart, int yEnd, int SliceIndex)
|
|
{
|
|
check(pInImage->Format == ERawImageFormat::BGRA8);
|
|
check((yStart % 4) == 0);
|
|
check((pInImage->SizeX % 4) == 0);
|
|
check((yStart >= 0) && (yStart <= pInImage->SizeY));
|
|
check((yEnd >= 0) && (yEnd <= pInImage->SizeY));
|
|
|
|
const int64 InStride = (int64)pInImage->SizeX * 4;
|
|
const int64 OutStride = (int64)pInImage->SizeX / 4 * BLOCK_SIZE_IN_BYTES;
|
|
const int64 InSliceSize = (int64)pInImage->SizeY * InStride;
|
|
const int64 OutSliceSize = (int64)pInImage->SizeY / 4 * OutStride;
|
|
|
|
uint8* pInTexels = reinterpret_cast<uint8*>(&pInImage->RawData[0]) + InSliceSize * SliceIndex;
|
|
uint8* pOutTexels = reinterpret_cast<uint8*>(&pOutImage->RawData[0]) + OutSliceSize * SliceIndex;
|
|
|
|
// Switch byte order for compressors input
|
|
for ( int y=yStart; y < yEnd; ++y )
|
|
{
|
|
uint8* pInTexelsSwap = pInTexels + (y * InStride);
|
|
for ( int x=0; x < pInImage->SizeX; ++x )
|
|
{
|
|
const uint8 r = pInTexelsSwap[0];
|
|
pInTexelsSwap[0] = pInTexelsSwap[2];
|
|
pInTexelsSwap[2] = r;
|
|
|
|
pInTexelsSwap += 4;
|
|
}
|
|
}
|
|
|
|
rgba_surface insurface;
|
|
insurface.ptr = pInTexels + (yStart * InStride);
|
|
insurface.width = pInImage->SizeX;
|
|
insurface.height = yEnd - yStart;
|
|
insurface.stride = pInImage->SizeX * 4;
|
|
|
|
pOutTexels += yStart / 4 * OutStride;
|
|
CompressBlocksBC7(&insurface, pOutTexels, pEncSettings);
|
|
}
|
|
|
|
#define MAX_QUALITY_BY_SIZE 4
|
|
#define FORCED_NORMAL_MAP_COMPRESSION_SIZE_VALUE 3
|
|
|
|
static uint16 GetDefaultCompressionBySizeValue(FCbObjectView InFormatConfigOverride)
|
|
{
|
|
if (InFormatConfigOverride)
|
|
{
|
|
// If we have an explicit format config, then use it directly
|
|
FCbFieldView FieldView = InFormatConfigOverride.FindView("DefaultASTCQualityBySize");
|
|
checkf(FieldView.HasValue(), TEXT("Missing DefaultASTCQualityBySize key from FormatConfigOverride"));
|
|
int32 CompressionModeValue = FieldView.AsInt32();
|
|
checkf(!FieldView.HasError(), TEXT("Failed to parse DefaultASTCQualityBySize value from FormatConfigOverride"));
|
|
return CompressionModeValue;
|
|
}
|
|
|
|
// start at default quality, then lookup in .ini file
|
|
int32 CompressionModeValue = 0;
|
|
GConfig->GetInt(TEXT("/Script/UnrealEd.CookerSettings"), TEXT("DefaultASTCQualityBySize"), CompressionModeValue, GEngineIni);
|
|
|
|
FParse::Value(FCommandLine::Get(), TEXT("-astcqualitybysize="), CompressionModeValue);
|
|
CompressionModeValue = FMath::Min<uint32>(CompressionModeValue, MAX_QUALITY_BY_SIZE);
|
|
|
|
return CompressionModeValue;
|
|
}
|
|
|
|
static EPixelFormat GetQualityFormat(int& BlockWidth, int& BlockHeight, const FCbObjectView& InFormatConfigOverride, int32 OverrideSizeValue = -1)
|
|
{
|
|
// Note: ISPC only supports 8x8 and higher quality, and only one speed (fast)
|
|
// convert to a string
|
|
EPixelFormat Format = PF_Unknown;
|
|
switch (OverrideSizeValue >= 0 ? OverrideSizeValue : GetDefaultCompressionBySizeValue(InFormatConfigOverride))
|
|
{
|
|
case 0: //Format = PF_ASTC_12x12; BlockWidth = BlockHeight = 12; break;
|
|
case 1: //Format = PF_ASTC_10x10; BlockWidth = BlockHeight = 10; break;
|
|
case 2: Format = PF_ASTC_8x8; BlockWidth = BlockHeight = 8; break;
|
|
case 3: Format = PF_ASTC_6x6; BlockWidth = BlockHeight = 6; break;
|
|
case 4: Format = PF_ASTC_4x4; BlockWidth = BlockHeight = 4; break;
|
|
default: UE_LOG(LogTemp, Fatal, TEXT("Max quality higher than expected"));
|
|
}
|
|
|
|
return Format;
|
|
}
|
|
|
|
struct FASTCEncoderSettings : public astc_enc_settings
|
|
{
|
|
FName TextureFormatName;
|
|
};
|
|
|
|
|
|
/**
|
|
* ASTC Compression function
|
|
*/
|
|
static void IntelASTCCompressScans(FASTCEncoderSettings* pEncSettings, FImage* pInImage, FCompressedImage2D* pOutImage, int yStart, int yEnd, int SliceIndex)
|
|
{
|
|
check(pInImage->Format == ERawImageFormat::BGRA8);
|
|
check((yStart % pEncSettings->block_height) == 0);
|
|
check((pInImage->SizeX % pEncSettings->block_width) == 0);
|
|
check((yStart >= 0) && (yStart <= pInImage->SizeY));
|
|
check((yEnd >= 0) && (yEnd <= pInImage->SizeY));
|
|
|
|
const int64 InStride = (int64)pInImage->SizeX * 4;
|
|
const int64 OutStride = (int64)pInImage->SizeX / pEncSettings->block_width * BLOCK_SIZE_IN_BYTES;
|
|
const int64 InSliceSize = (int64)pInImage->SizeY * InStride;
|
|
const int64 OutSliceSize = (int64)pInImage->SizeY / pEncSettings->block_height * OutStride;
|
|
|
|
uint8* pInTexels = reinterpret_cast<uint8*>(&pInImage->RawData[0]) + InSliceSize * SliceIndex;
|
|
uint8* pOutTexels = reinterpret_cast<uint8*>(&pOutImage->RawData[0]) + OutSliceSize * SliceIndex;
|
|
|
|
if (pEncSettings->TextureFormatName == GTextureFormatNameASTC_RGB)
|
|
{
|
|
// Switch byte order for compressors input (BGRA -> RGBA)
|
|
// Force A=255
|
|
for (int y = yStart; y < yEnd; ++y)
|
|
{
|
|
uint8* pInTexelsSwap = pInTexels + (y * InStride);
|
|
for (int x = 0; x < pInImage->SizeX; ++x)
|
|
{
|
|
const uint8 r = pInTexelsSwap[0];
|
|
pInTexelsSwap[0] = pInTexelsSwap[2];
|
|
pInTexelsSwap[2] = r;
|
|
pInTexelsSwap[3] = 255;
|
|
|
|
pInTexelsSwap += 4;
|
|
}
|
|
}
|
|
}
|
|
else if (pEncSettings->TextureFormatName == GTextureFormatNameASTC_RGBA)
|
|
{
|
|
// Switch byte order for compressors input (BGRA -> RGBA)
|
|
for (int y = yStart; y < yEnd; ++y)
|
|
{
|
|
uint8* pInTexelsSwap = pInTexels + (y * InStride);
|
|
for (int x = 0; x < pInImage->SizeX; ++x)
|
|
{
|
|
const uint8 r = pInTexelsSwap[0];
|
|
pInTexelsSwap[0] = pInTexelsSwap[2];
|
|
pInTexelsSwap[2] = r;
|
|
|
|
pInTexelsSwap += 4;
|
|
}
|
|
}
|
|
}
|
|
else if (pEncSettings->TextureFormatName == GTextureFormatNameASTC_NormalAG)
|
|
{
|
|
// Switch byte order for compressors input (BGRA -> RGBA)
|
|
// Re-normalize
|
|
// Set any unused RGB components to 0, an unused A to 255.
|
|
for (int y = yStart; y < yEnd; ++y)
|
|
{
|
|
uint8* pInTexelsSwap = pInTexels + (y * InStride);
|
|
for (int x = 0; x < pInImage->SizeX; ++x)
|
|
{
|
|
FVector Normal = FVector(pInTexelsSwap[2] / 255.0f * 2.0f - 1.0f, pInTexelsSwap[1] / 255.0f * 2.0f - 1.0f, pInTexelsSwap[0] / 255.0f * 2.0f - 1.0f);
|
|
Normal = Normal.GetSafeNormal();
|
|
pInTexelsSwap[0] = 0;
|
|
pInTexelsSwap[1] = FMath::RoundToInt((Normal.Y * 0.5f + 0.5f) * 255.f);
|
|
pInTexelsSwap[2] = 0;
|
|
pInTexelsSwap[3] = FMath::RoundToInt((Normal.X * 0.5f + 0.5f) * 255.f);
|
|
|
|
pInTexelsSwap += 4;
|
|
}
|
|
}
|
|
}
|
|
else if (pEncSettings->TextureFormatName == GTextureFormatNameASTC_NormalRG)
|
|
{
|
|
// Switch byte order for compressors input (BGRA -> RGBA)
|
|
// Re-normalize
|
|
// Set any unused RGB components to 0, an unused A to 255.
|
|
for (int y = yStart; y < yEnd; ++y)
|
|
{
|
|
uint8* pInTexelsSwap = pInTexels + (y * InStride);
|
|
for (int x = 0; x < pInImage->SizeX; ++x)
|
|
{
|
|
FVector Normal = FVector(pInTexelsSwap[2] / 255.0f * 2.0f - 1.0f, pInTexelsSwap[1] / 255.0f * 2.0f - 1.0f, pInTexelsSwap[0] / 255.0f * 2.0f - 1.0f);
|
|
Normal = Normal.GetSafeNormal();
|
|
pInTexelsSwap[0] = FMath::RoundToInt((Normal.X * 0.5f + 0.5f) * 255.f);
|
|
pInTexelsSwap[1] = FMath::RoundToInt((Normal.Y * 0.5f + 0.5f) * 255.f);
|
|
pInTexelsSwap[2] = 0;
|
|
pInTexelsSwap[3] = 255;
|
|
|
|
pInTexelsSwap += 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
rgba_surface insurface;
|
|
insurface.ptr = pInTexels + (yStart * InStride);
|
|
insurface.width = pInImage->SizeX;
|
|
insurface.height = yEnd - yStart;
|
|
insurface.stride = pInImage->SizeX * 4;
|
|
|
|
pOutTexels += yStart / pEncSettings->block_height * OutStride;
|
|
CompressBlocksASTC(&insurface, pOutTexels, pEncSettings);
|
|
}
|
|
|
|
/**
|
|
* Intel BC texture format handler.
|
|
*/
|
|
class FTextureFormatIntelISPCTexComp : public ITextureFormat
|
|
{
|
|
public:
|
|
FTextureFormatIntelISPCTexComp()
|
|
{
|
|
}
|
|
virtual ~FTextureFormatIntelISPCTexComp()
|
|
{
|
|
}
|
|
virtual bool AllowParallelBuild() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
virtual FCbObject ExportGlobalFormatConfig(const FTextureBuildSettings& BuildSettings) const override
|
|
{
|
|
FCbWriter Writer;
|
|
Writer.BeginObject("TextureFormatIntelISPCTexCompSettings");
|
|
Writer.AddInteger("DefaultASTCQualityBySize", GetDefaultCompressionBySizeValue(FCbObjectView()));
|
|
Writer.EndObject();
|
|
return Writer.Save().AsObject();
|
|
}
|
|
|
|
// Return the version for the DX11 formats BC6H and BC7 (not ASTC)
|
|
virtual uint16 GetVersion(
|
|
FName Format,
|
|
const FTextureBuildSettings* BuildSettings = nullptr
|
|
) const override
|
|
{
|
|
return BASE_ISPC_DX11_FORMAT_VERSION;
|
|
}
|
|
|
|
virtual void GetSupportedFormats(TArray<FName>& OutFormats) const override
|
|
{
|
|
for (int32 i = 0; i < UE_ARRAY_COUNT(GSupportedTextureFormatNames); ++i)
|
|
{
|
|
OutFormats.Add(GSupportedTextureFormatNames[i]);
|
|
}
|
|
}
|
|
|
|
virtual FTextureFormatCompressorCaps GetFormatCapabilities() const override
|
|
{
|
|
return FTextureFormatCompressorCaps(); // Default capabilities.
|
|
}
|
|
|
|
static void SetupScans(const FImage& InImage, int BlockWidth, int BlockHeight, FCompressedImage2D& OutCompressedImage, FMultithreadSettings &MultithreadSettings)
|
|
{
|
|
const int AlignedSizeX = AlignArbitrary(InImage.SizeX, BlockWidth);
|
|
const int AlignedSizeY = AlignArbitrary(InImage.SizeY, BlockHeight);
|
|
const int WidthInBlocks = AlignedSizeX / BlockWidth;
|
|
const int HeightInBlocks = AlignedSizeY / BlockHeight;
|
|
const int64 SizePerSlice = (int64)WidthInBlocks * HeightInBlocks * BLOCK_SIZE_IN_BYTES;
|
|
OutCompressedImage.RawData.AddUninitialized(SizePerSlice * InImage.NumSlices);
|
|
OutCompressedImage.SizeX = FMath::Max(AlignedSizeX, BlockWidth);
|
|
OutCompressedImage.SizeY = FMath::Max(AlignedSizeY, BlockHeight);
|
|
|
|
// When we allow async tasks to execute we do so with BlockHeight lines of the image per task
|
|
// This isn't optimal for long thin textures, but works well with how ISPC works
|
|
MultithreadSettings.iScansPerTask = BlockHeight;
|
|
MultithreadSettings.iNumTasks = FMath::Max((AlignedSizeY / MultithreadSettings.iScansPerTask) - 1, 0);
|
|
}
|
|
|
|
static void PadImageToBlockSize(FImage &InOutImage, int BlockWidth, int BlockHeight, int BytesPerPixel)
|
|
{
|
|
const int AlignedSizeX = AlignArbitrary(InOutImage.SizeX, BlockWidth);
|
|
const int AlignedSizeY = AlignArbitrary(InOutImage.SizeY, BlockHeight);
|
|
const int64 AlignedSliceSize = (int64)AlignedSizeX * AlignedSizeY * BytesPerPixel;
|
|
const int64 AlignedTotalSize = AlignedSliceSize * InOutImage.NumSlices;
|
|
const int64 OriginalSliceSize = (int64)InOutImage.SizeX * InOutImage.SizeY * BytesPerPixel;
|
|
|
|
// Early out if no padding is necessary
|
|
if (AlignedSizeX == InOutImage.SizeX && AlignedSizeY == InOutImage.SizeY)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Allocate temp buffer
|
|
//@TODO: Optimize away this temp buffer (could avoid last FMemory::Memcpy)
|
|
TArray64<uint8> TempBuffer;
|
|
TempBuffer.SetNumUninitialized(AlignedTotalSize);
|
|
|
|
const int PaddingX = AlignedSizeX - InOutImage.SizeX;
|
|
const int PaddingY = AlignedSizeY - InOutImage.SizeY;
|
|
const int SrcStride = InOutImage.SizeX * BytesPerPixel;
|
|
const int DstStride = AlignedSizeX * BytesPerPixel;
|
|
|
|
for (int SliceIndex = 0; SliceIndex < InOutImage.NumSlices; ++SliceIndex)
|
|
{
|
|
uint8* DstData = ((uint8*)TempBuffer.GetData()) + SliceIndex * AlignedSliceSize;
|
|
const uint8* SrcData = ((uint8*)InOutImage.RawData.GetData()) + SliceIndex * OriginalSliceSize;
|
|
|
|
// Copy all of SrcData and pad on X-axis:
|
|
for (int Y = 0; Y < InOutImage.SizeY; ++Y)
|
|
{
|
|
FMemory::Memcpy(DstData, SrcData, SrcStride);
|
|
SrcData += SrcStride - BytesPerPixel; // Src: Last pixel on this row
|
|
DstData += SrcStride; // Dst: Beginning of the padded region at the end of this row
|
|
for (int PadX = 0; PadX < PaddingX; PadX++)
|
|
{
|
|
// Replicate right-most pixel as padding on X-axis
|
|
FMemory::Memcpy(DstData, SrcData, BytesPerPixel);
|
|
DstData += BytesPerPixel;
|
|
}
|
|
SrcData += BytesPerPixel; // Src & Dst: Beginning of next row
|
|
}
|
|
|
|
// Replicate last row as padding on Y-axis:
|
|
SrcData = DstData - DstStride; // Src: Beginning of the last row (of DstData)
|
|
for (int PadY = 0; PadY < PaddingY; PadY++)
|
|
{
|
|
FMemory::Memcpy(DstData, SrcData, DstStride);
|
|
DstData += DstStride; // Dst: Beginning of the padded region at the end of this row
|
|
}
|
|
}
|
|
|
|
// Replace InOutImage with the new data
|
|
InOutImage.RawData = MoveTemp(TempBuffer);
|
|
InOutImage.SizeX = AlignedSizeX;
|
|
InOutImage.SizeY = AlignedSizeY;
|
|
}
|
|
|
|
/**
|
|
Remove Float16 values, which aren't correctly handled by the ISPCTextureCompressor.
|
|
https://docs.microsoft.com/en-us/windows/desktop/direct3d11/bc6h-format
|
|
*/
|
|
static void SanitizeFloat16ForBC6H(FImage& InOutImage)
|
|
{
|
|
check(InOutImage.Format == ERawImageFormat::RGBA16F);
|
|
|
|
const int64 TexelNum = InOutImage.RawData.Num() / sizeof(FFloat16);
|
|
FFloat16* Data = reinterpret_cast<FFloat16*>(&InOutImage.RawData[0]);
|
|
for (int64 TexelIndex = 0; TexelIndex < TexelNum; ++TexelIndex)
|
|
{
|
|
// Flush negative values to 0, as those aren't supported by BC6H_UF16.
|
|
FFloat16& F16Value = Data[TexelIndex];
|
|
|
|
if ( F16Value.IsNegative() )
|
|
{
|
|
F16Value.Encoded = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual EPixelFormat GetPixelFormatForImage(const FTextureBuildSettings& BuildSettings, const struct FImage& Image, bool bImageHasAlphaChannel) const override
|
|
{
|
|
if (BuildSettings.TextureFormatName == GTextureFormatNameBC6H)
|
|
{
|
|
return PF_BC6H;
|
|
}
|
|
else if (BuildSettings.TextureFormatName == GTextureFormatNameBC7)
|
|
{
|
|
return PF_BC7;
|
|
}
|
|
else if (BuildSettings.bVirtualStreamable)
|
|
{
|
|
return PF_ASTC_4x4;
|
|
}
|
|
else
|
|
{
|
|
int _Width, _Height;
|
|
bool bIsNormalMap = (BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalAG ||
|
|
BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalRG);
|
|
|
|
return GetQualityFormat(_Width, _Height, BuildSettings.FormatConfigOverride, bIsNormalMap ? FORCED_NORMAL_MAP_COMPRESSION_SIZE_VALUE : BuildSettings.CompressionQuality);
|
|
}
|
|
}
|
|
|
|
virtual bool CompressImage(
|
|
const FImage& InImage,
|
|
const FTextureBuildSettings& BuildSettings,
|
|
bool bImageHasAlphaChannel,
|
|
FCompressedImage2D& OutCompressedImage
|
|
) const override
|
|
{
|
|
check(InImage.SizeX > 0);
|
|
check(InImage.SizeY > 0);
|
|
check(InImage.NumSlices > 0);
|
|
|
|
bool bCompressionSucceeded = false;
|
|
|
|
int BlockWidth = 0;
|
|
int BlockHeight = 0;
|
|
|
|
const bool bUseTasks = true;
|
|
FMultithreadSettings MultithreadSettings;
|
|
|
|
EPixelFormat CompressedPixelFormat = GetPixelFormatForImage(BuildSettings, InImage, bImageHasAlphaChannel);
|
|
|
|
if ( BuildSettings.TextureFormatName == GTextureFormatNameBC6H )
|
|
{
|
|
FImage Image;
|
|
InImage.CopyTo(Image, ERawImageFormat::RGBA16F, EGammaSpace::Linear);
|
|
|
|
SanitizeFloat16ForBC6H(Image);
|
|
|
|
bc6h_enc_settings settings;
|
|
GetProfile_bc6h_basic(&settings);
|
|
|
|
SetupScans(Image, 4, 4, OutCompressedImage, MultithreadSettings);
|
|
PadImageToBlockSize(Image, 4, 4, 4*2);
|
|
FMultithreadedCompression<bc6h_enc_settings>::Compress(MultithreadSettings, settings, Image, OutCompressedImage, &IntelBC6HCompressScans, bUseTasks);
|
|
|
|
bCompressionSucceeded = true;
|
|
}
|
|
else if ( BuildSettings.TextureFormatName == GTextureFormatNameBC7 )
|
|
{
|
|
FImage Image;
|
|
InImage.CopyTo(Image, ERawImageFormat::BGRA8, BuildSettings.GetGammaSpace());
|
|
|
|
bc7_enc_settings settings;
|
|
if ( bImageHasAlphaChannel )
|
|
{
|
|
GetProfile_alpha_basic(&settings);
|
|
}
|
|
else
|
|
{
|
|
GetProfile_basic(&settings);
|
|
}
|
|
|
|
SetupScans(Image, 4, 4, OutCompressedImage, MultithreadSettings);
|
|
PadImageToBlockSize(Image, 4, 4, 4*1);
|
|
FMultithreadedCompression<bc7_enc_settings>::Compress(MultithreadSettings, settings, Image, OutCompressedImage, &IntelBC7CompressScans, bUseTasks);
|
|
|
|
bCompressionSucceeded = true;
|
|
}
|
|
else
|
|
{
|
|
bool bIsRGBColorASTC = (BuildSettings.TextureFormatName == GTextureFormatNameASTC_RGB ||
|
|
((BuildSettings.TextureFormatName == GTextureFormatNameASTC_RGBAuto) && !bImageHasAlphaChannel));
|
|
bool bIsRGBAColorASTC = (BuildSettings.TextureFormatName == GTextureFormatNameASTC_RGBA ||
|
|
((BuildSettings.TextureFormatName == GTextureFormatNameASTC_RGBAuto) && bImageHasAlphaChannel));
|
|
bool bIsNormalMap = (BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalAG ||
|
|
BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalRG);
|
|
|
|
if (BuildSettings.bVirtualStreamable)
|
|
{
|
|
// Always use 4x4 for streamable VT, to reduce texture format fragmentation
|
|
BlockWidth = 4;
|
|
BlockHeight = 4;
|
|
}
|
|
else
|
|
{
|
|
GetQualityFormat( BlockWidth, BlockHeight, BuildSettings.FormatConfigOverride, bIsNormalMap ? FORCED_NORMAL_MAP_COMPRESSION_SIZE_VALUE : BuildSettings.CompressionQuality );
|
|
}
|
|
check(CompressedPixelFormat == PF_ASTC_4x4 || !BuildSettings.bVirtualStreamable);
|
|
|
|
FASTCEncoderSettings EncoderSettings;
|
|
if (BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalAG)
|
|
{
|
|
GetProfile_astc_alpha_fast(&EncoderSettings, BlockWidth, BlockHeight);
|
|
EncoderSettings.TextureFormatName = BuildSettings.TextureFormatName;
|
|
bCompressionSucceeded = true;
|
|
check(EncoderSettings.block_width!=0);
|
|
}
|
|
else if (BuildSettings.TextureFormatName == GTextureFormatNameASTC_NormalRG)
|
|
{
|
|
GetProfile_astc_fast(&EncoderSettings, BlockWidth, BlockHeight);
|
|
EncoderSettings.TextureFormatName = BuildSettings.TextureFormatName;
|
|
bCompressionSucceeded = true;
|
|
check(EncoderSettings.block_width!=0);
|
|
}
|
|
else if (bIsRGBColorASTC)
|
|
{
|
|
GetProfile_astc_fast(&EncoderSettings, BlockWidth, BlockHeight);
|
|
EncoderSettings.TextureFormatName = GTextureFormatNameASTC_RGB;
|
|
bCompressionSucceeded = true;
|
|
check(EncoderSettings.block_width!=0);
|
|
}
|
|
else if (bIsRGBAColorASTC)
|
|
{
|
|
GetProfile_astc_alpha_fast(&EncoderSettings, BlockWidth, BlockHeight);
|
|
EncoderSettings.TextureFormatName = GTextureFormatNameASTC_RGBA;
|
|
bCompressionSucceeded = true;
|
|
check(EncoderSettings.block_width!=0);
|
|
}
|
|
else
|
|
{
|
|
check(0);
|
|
}
|
|
|
|
if (bCompressionSucceeded)
|
|
{
|
|
FImage Image;
|
|
InImage.CopyTo(Image, ERawImageFormat::BGRA8, BuildSettings.GetGammaSpace());
|
|
|
|
SetupScans(Image, EncoderSettings.block_width, EncoderSettings.block_height, OutCompressedImage, MultithreadSettings);
|
|
PadImageToBlockSize(Image, EncoderSettings.block_width, EncoderSettings.block_height, 4 * 1);
|
|
|
|
#if DEBUG_SAVE_INTERMEDIATE_IMAGES
|
|
//@DEBUG (save padded input as BMP):
|
|
static bool SaveInputOutput = false;
|
|
static volatile int32 Counter = 0;
|
|
int LocalCounter = Counter;
|
|
if (SaveInputOutput) // && LocalCounter < 10 && Image.SizeX >= 1024)
|
|
{
|
|
const FString FileName = FString::Printf(TEXT("Smedis-Input-%d.bmp"), FPlatformTLS::GetCurrentThreadId());
|
|
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*FileName);
|
|
SaveImageAsBMP(*FileWriter, Image.RawData.GetData(), 4, Image.SizeX, Image.SizeY);
|
|
delete FileWriter;
|
|
FPlatformAtomics::InterlockedIncrement(&Counter);
|
|
}
|
|
#endif
|
|
|
|
FMultithreadedCompression<FASTCEncoderSettings>::Compress(MultithreadSettings, EncoderSettings, Image, OutCompressedImage, &IntelASTCCompressScans, bUseTasks);
|
|
|
|
#if DEBUG_SAVE_INTERMEDIATE_IMAGES
|
|
//@DEBUG (save swizzled/fixed-up input as BMP):
|
|
if (SaveInputOutput) // && LocalCounter < 10 && Image.SizeX >= 1024)
|
|
{
|
|
const FString FileName = FString::Printf(TEXT("Smedis-InputSwizzled-%d.bmp"), FPlatformTLS::GetCurrentThreadId());
|
|
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*FileName);
|
|
SaveImageAsBMP(*FileWriter, Image.RawData.GetData(), 4, Image.SizeX, Image.SizeY);
|
|
delete FileWriter;
|
|
FPlatformAtomics::InterlockedIncrement(&Counter);
|
|
}
|
|
|
|
//@DEBUG (save output as .astc file):
|
|
if (SaveInputOutput)// && LocalCounter < 10 && Image.SizeX >= 1024)
|
|
{
|
|
const FString FileName = FString::Printf(TEXT("Smedis-Output-%d.astc"), FPlatformTLS::GetCurrentThreadId());
|
|
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*FileName);
|
|
SaveImageAsASTC(*FileWriter, OutCompressedImage.RawData.GetData(), OutCompressedImage.SizeX, OutCompressedImage.SizeY, EncoderSettings.block_width, EncoderSettings.block_height);
|
|
delete FileWriter;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
OutCompressedImage.PixelFormat = CompressedPixelFormat;
|
|
OutCompressedImage.SizeX = InImage.SizeX;
|
|
OutCompressedImage.SizeY = InImage.SizeY;
|
|
OutCompressedImage.SizeZ = (BuildSettings.bVolume || BuildSettings.bTextureArray) ? InImage.NumSlices : 1;
|
|
return bCompressionSucceeded;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Module for DXT texture compression.
|
|
*/
|
|
static ITextureFormat* Singleton = nullptr;
|
|
|
|
class FTextureFormatIntelISPCTexCompModule : public ITextureFormatModule
|
|
{
|
|
public:
|
|
FTextureFormatIntelISPCTexCompModule()
|
|
{
|
|
mDllHandle = nullptr;
|
|
}
|
|
|
|
virtual ~FTextureFormatIntelISPCTexCompModule()
|
|
{
|
|
delete Singleton;
|
|
Singleton = nullptr;
|
|
|
|
if ( mDllHandle != nullptr )
|
|
{
|
|
FPlatformProcess::FreeDllHandle(mDllHandle);
|
|
mDllHandle = nullptr;
|
|
}
|
|
}
|
|
|
|
virtual ITextureFormat* GetTextureFormat()
|
|
{
|
|
if (!Singleton)
|
|
{
|
|
FString DLLPath;
|
|
#if PLATFORM_WINDOWS
|
|
#if PLATFORM_64BITS
|
|
DLLPath = FPaths::EngineDir() / TEXT("Binaries/ThirdParty/Intel/ISPCTexComp/Win64-Release/ispc_texcomp.dll");
|
|
#else //32-bit platform
|
|
DLLPath = FPaths::EngineDir() / TEXT("Binaries/ThirdParty/Intel/ISPCTexComp/Win32-Release/ispc_texcomp.dll");
|
|
#endif
|
|
#elif PLATFORM_MAC
|
|
DLLPath = TEXT("libispc_texcomp.dylib");
|
|
#elif PLATFORM_LINUX
|
|
DLLPath = FPaths::EngineDir() / TEXT("Binaries/ThirdParty/Intel/ISPCTexComp/Linux64-Release/libispc_texcomp.so");
|
|
#endif
|
|
|
|
if (DLLPath.Len() > 0)
|
|
{
|
|
mDllHandle = FPlatformProcess::GetDllHandle(*DLLPath);
|
|
UE_CLOG(mDllHandle == nullptr, LogTextureFormatIntelISPCTexComp, Warning, TEXT("Unable to load %s"), *DLLPath);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogTextureFormatIntelISPCTexComp, Warning, TEXT("Platform does not have an ispc_texcomp DLL/library"));
|
|
}
|
|
|
|
Singleton = new FTextureFormatIntelISPCTexComp();
|
|
}
|
|
return Singleton;
|
|
}
|
|
|
|
static inline UE::DerivedData::TBuildFunctionFactory<FIntelISPCTexCompTextureBuildFunction> BuildFunctionFactory;
|
|
void* mDllHandle;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FTextureFormatIntelISPCTexCompModule, TextureFormatIntelISPCTexComp);
|