Files
2025-05-18 13:04:45 +08:00

464 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ExrReaderGpu.h"
#if PLATFORM_WINDOWS
#include "ExrReaderGpuModule.h"
#include "HAL/IConsoleManager.h"
PRAGMA_DEFAULT_VISIBILITY_START
THIRD_PARTY_INCLUDES_START
#include "Imath/ImathBox.h"
#include "OpenEXR/ImfCompressionAttribute.h"
#include "OpenEXR/ImfHeader.h"
#include "OpenEXR/ImfRgbaFile.h"
#include "OpenEXR/ImfStandardAttributes.h"
#include "OpenEXR/ImfVersion.h"
THIRD_PARTY_INCLUDES_END
PRAGMA_DEFAULT_VISIBILITY_END
static TAutoConsoleVariable<bool> CVarForceTileDescBufferExrGpuReader(
TEXT("r.ExrReaderGPU.ForceTileDescBuffer"),
true,
TEXT("Calculates tile description and offsets on CPU and provides a Structured buffer.\n")
TEXT("to be used to access tile description on GPU\n"));
namespace
{
bool ReadBytes(FILE* FileHandle, char* Buffer, int32 Length)
{
int32 Result = -1;
try
{
Result = fread(Buffer, 1, Length, FileHandle);
}
catch (...)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Error reading bytes"));
return false;
}
if (Result != Length)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Error reading file %i %i"), ferror(FileHandle), feof(FileHandle));
return false;
}
return true;
}
template <int32 Length>
bool CheckIsNullTerminated(const char(&StringToCheck)[Length])
{
for (int32 i = 0; i < Length; ++i)
{
if (StringToCheck[i] == '\0')
{
return true;
}
}
// The string was never terminated.
UE_LOG(LogExrReaderGpu, Error, TEXT("Invalid EXR file with string that was never terminated"));
return false;
}
bool Read4ByteValue(FILE* In, int32& OutValue)
{
char ThrowAwayValue[4];
if (!ReadBytes(In, ThrowAwayValue, 4))
{
return false;
}
OutValue = (static_cast <unsigned char> (ThrowAwayValue[0]) & 0x000000ff) |
((static_cast <unsigned char> (ThrowAwayValue[1]) << 8) & 0x0000ff00) |
((static_cast <unsigned char> (ThrowAwayValue[2]) << 16) & 0x00ff0000) |
(static_cast <unsigned char> (ThrowAwayValue[3]) << 24);
return true;
}
bool Read8ByteValue(FILE* InFileHandle, int64& OutValue)
{
unsigned char ThrowAwayValue[8];
if (!ReadBytes(InFileHandle, reinterpret_cast<char*>(ThrowAwayValue), 8))
{
return false;
}
OutValue = ((uint64)ThrowAwayValue[0] & 0x00000000000000ffLL) |
(((uint64)ThrowAwayValue[1] << 8) & 0x000000000000ff00LL) |
(((uint64)ThrowAwayValue[2] << 16) & 0x0000000000ff0000LL) |
(((uint64)ThrowAwayValue[3] << 24) & 0x00000000ff000000LL) |
(((uint64)ThrowAwayValue[4] << 32) & 0x000000ff00000000LL) |
(((uint64)ThrowAwayValue[5] << 40) & 0x0000ff0000000000LL) |
(((uint64)ThrowAwayValue[6] << 48) & 0x00ff000000000000LL) |
((uint64)ThrowAwayValue[7] << 56);
return true;
}
bool ReadString(FILE* InFileHandle, int32 Length, char OutValue[])
{
while (Length >= 0)
{
if (!ReadBytes(InFileHandle, OutValue, 1))
{
return false;
}
if (*OutValue == 0)
{
break;
}
--Length;
++OutValue;
}
return true;
}
bool ReadString(FILE* InFileHandle, int32 InSize)
{
TArray<char> Value;
Value.SetNum(InSize);
for (int32 CharNum = 0; CharNum < InSize; CharNum++)
{
if (!ReadBytes(InFileHandle, &Value[CharNum], 1))
{
return false;
}
}
return true;
}
}
bool FExrReader::ReadMagicNumberAndVersionField(FILE* FileHandle)
{
using namespace OPENEXR_IMF_INTERNAL_NAMESPACE;
int32 Magic;
int32 Version;
if (!Read4ByteValue(FileHandle, Magic))
{
return false;
}
Read4ByteValue(FileHandle, Version);
if (Magic != MAGIC || getVersion(Version) != EXR_VERSION || !supportsFlags(getFlags(Version)))
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Invalid EXR file has been detected."));
return false;
}
return true;
}
bool FExrReader::ReadHeaderData(FILE* FileHandle)
{
int32 AttrCount = 0;
bool bNothingIsRead = false;
// Read all header attributes.
while (true)
{
char AttributeName[STRING_SIZE];
ReadString(FileHandle, MAX_LENGTH, AttributeName);
// Check to make sure it is not null. And if it is - its the end of the header.
if (AttributeName[0] == 0)
{
if (AttrCount == 0)
{
bNothingIsRead = true;
}
break;
}
AttrCount++;
// Read the attribute type and the size of the attribute value.
char TypeName[STRING_SIZE];
int32 Size;
ReadString(FileHandle, MAX_LENGTH, TypeName);
CheckIsNullTerminated(TypeName);
Read4ByteValue(FileHandle, Size);
if (Size < 0)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Invalid EXR file has been detected."));
return false;
}
try
{
ReadString(FileHandle, Size);
}
catch (...)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Issue reading attribute from EXR: %hs"), AttributeName);
return false;
}
}
return true;
}
bool FExrReader::ReadLineOrTileOffsets(FILE* FileHandle, ELineOrder LineOrder, TArray<int64>& LineOrTileOffsets)
{
// At the moment we only support increasing Y EXR;
// LineOrder is currently unused but we might need to take that into account as exr scanlines can go from top to bottom and vice versa.
if (LineOrder != INCREASING_Y)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Unsupported Line order in EXR: %d"), LineOrder);
return false;
}
for (int32 i = 0; i < LineOrTileOffsets.Num(); i++)
{
Read8ByteValue(FileHandle, LineOrTileOffsets[i]);
}
return true;
}
bool FExrReader::GenerateTextureData(uint16* Buffer, int32 BufferSize, FString FilePath, int32 NumberOfScanlines, int32 NumChannels)
{
check(Buffer != nullptr);
FILE* FileHandle;
errno_t Error = fopen_s(&FileHandle, TCHAR_TO_ANSI(*FilePath), "rb");
if (FileHandle == NULL || Error != 0)
{
return false;
}
fseek(FileHandle, 0, SEEK_END);
int64 FileLength = ftell(FileHandle);
rewind(FileHandle);
// Reading header (throwaway) and line offset data. We just need line offset data.
TArray<int64> LineOrTileOffsets;
{
ReadMagicNumberAndVersionField(FileHandle);
ReadHeaderData(FileHandle);
LineOrTileOffsets.SetNum(NumberOfScanlines);
// At the moment we support only increasing Y.
ELineOrder LineOrder = INCREASING_Y;
ReadLineOrTileOffsets(FileHandle, LineOrder, LineOrTileOffsets);
}
fseek(FileHandle, LineOrTileOffsets[0], SEEK_SET);
fread(Buffer, BufferSize, 1 /*NumOfElements*/, FileHandle);
fclose(FileHandle);
return true;
}
void FExrReader::CalculateTileOffsets
( TArray<int32>& OutNumTilesPerLevel
, TArray<TArray<FTileDesc>>& OutPartialTileInfo
, const FIntPoint& FullTextureResolution
, const FIntPoint& TileDimWithBorders
, int32 NumMipLevels
, int64 PixelSize)
{
TRACE_CPUPROFILER_EVENT_SCOPE(CalculateTileOffsets);
int64 CurrentPosition = 0;
for (int32 MipLevel = 0; MipLevel < NumMipLevels; MipLevel++)
{
int32 MipDiv = 1 << MipLevel;
// Resolution of the texture in pixels for this mip level.
FIntPoint MipResolution = FullTextureResolution / MipDiv;
// Dimension of the texture in tiles including partial tiles
float TileResFractionX = float(MipResolution.X) / TileDimWithBorders.X;
float TileResFractionY = float(MipResolution.Y) / TileDimWithBorders.Y;
FIntPoint DimensionInTiles_PartialTiles
( FMath::CeilToInt(TileResFractionX)
, FMath::CeilToInt(TileResFractionY));
// Dimension of the texture in tiles excluding partial tiles.
FIntPoint DimensionInTiles_CompleteTiles
( FMath::FloorToInt(TileResFractionX)
, FMath::FloorToInt(TileResFractionY));
// Total number of tiles for this mip level.
int32 NumActualTiles = DimensionInTiles_PartialTiles.X * DimensionInTiles_PartialTiles.Y;
OutNumTilesPerLevel.Add(DimensionInTiles_PartialTiles.X * DimensionInTiles_PartialTiles.Y);
const bool bHasPartialTiles = (DimensionInTiles_PartialTiles != DimensionInTiles_CompleteTiles) || CVarForceTileDescBufferExrGpuReader.GetValueOnAnyThread();
if (!bHasPartialTiles)
{
continue;
}
OutPartialTileInfo.Add({});
TArray<FTileDesc>& TileInfoList = OutPartialTileInfo[MipLevel];
TileInfoList.SetNum(NumActualTiles);
// Resolution of the partial tile in the bottom right corner.
const FIntPoint PartialTileResolution = FIntPoint(MipResolution.X % TileDimWithBorders.X, MipResolution.Y % TileDimWithBorders.Y);
const int64 MipOffsetStart = CurrentPosition;
for (int TileIndex = 0; TileIndex < NumActualTiles; TileIndex++)
{
FIntPoint TileDim(TileDimWithBorders);
if (DimensionInTiles_PartialTiles.X != DimensionInTiles_CompleteTiles.X)
{
// If true - this is a partial tile in X dimension.
if (((TileIndex + 1) % DimensionInTiles_PartialTiles.X) == 0)
{
TileDim.X = PartialTileResolution.X;
}
}
if (DimensionInTiles_PartialTiles.Y != DimensionInTiles_CompleteTiles.Y)
{
// If true - this is a partial tile in Y dimension.
if (TileIndex >= (DimensionInTiles_PartialTiles.X * DimensionInTiles_CompleteTiles.Y))
{
TileDim.Y = PartialTileResolution.Y;
}
}
TileInfoList[TileIndex] = { TileDim, (uint32)(CurrentPosition - MipOffsetStart) };
// Tile offset.
CurrentPosition += TileDim.X * TileDim.Y * PixelSize;
// Vanilla exr has 20 byte padding at the beginning of each tile.
CurrentPosition += FExrReader::TILE_PADDING;
}
}
}
bool FExrReader::OpenExrAndPrepareForPixelReading(FString FilePath, const TArray<int32>& NumOffsetsPerLevel)
{
if (FileHandle != nullptr)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("The file has already been open for reading but never closed."));
return false;
}
errno_t Error = fopen_s(&FileHandle, TCHAR_TO_ANSI(*FilePath), "rb");
if (FileHandle == NULL || Error != 0)
{
return false;
}
// Reading header (throwaway) and line offset data. We just need line offset data.
{
ReadMagicNumberAndVersionField(FileHandle);
if (!ReadHeaderData(FileHandle))
{
return false;
}
LineOrTileOffsetsPerLevel.SetNum(NumOffsetsPerLevel.Num());
// At the moment we support only increasing Y.
ELineOrder LineOrder = INCREASING_Y;
for (int Level = 0; Level < LineOrTileOffsetsPerLevel.Num(); Level++)
{
TArray<int64>& OffsetsPerLevel = LineOrTileOffsetsPerLevel[Level];
OffsetsPerLevel.SetNum(NumOffsetsPerLevel[Level]);
ReadLineOrTileOffsets(FileHandle, LineOrder, OffsetsPerLevel);
}
}
fseek(FileHandle, 0, SEEK_END);
FileLength = ftell(FileHandle);
fseek(FileHandle, LineOrTileOffsetsPerLevel[0][0], SEEK_SET);
return true;
}
bool FExrReader::ReadExrImageChunk(void* Buffer, int64 ChunkSize)
{
if (FileHandle == nullptr)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("File is not open for reading. Please use OpenExrAndPrepareForPixelReading. "));
return false;
}
if (Buffer == nullptr)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Buffer provided is invalid. Please provide a valid buffer. "));
return false;
}
size_t NumElements = 1;
size_t NumElementsRead = fread(Buffer, ChunkSize, NumElements, FileHandle);
if (NumElementsRead != NumElements)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Issue reading EXR chunk. "));
}
return NumElementsRead == NumElements;
}
bool FExrReader::SeekTileWithinFile(const int32 StartTileIndex, const int32 Level, int64& OutBufferOffset)
{
if (StartTileIndex >= LineOrTileOffsetsPerLevel[Level].Num())
{
UE_LOG(LogExrReaderGpu, Error, TEXT("Tile index is invalid."));
return false;
}
int64 LineOffset = LineOrTileOffsetsPerLevel[Level][StartTileIndex];
OutBufferOffset = LineOffset - LineOrTileOffsetsPerLevel[Level][0];
return fseek(FileHandle, LineOffset, SEEK_SET) == 0;
}
bool FExrReader::GetByteOffsetForTile(const int32 TileIndex, const int32 Level, int64& OutBufferOffset)
{
if (LineOrTileOffsetsPerLevel.Num() <= Level || LineOrTileOffsetsPerLevel[Level].Num() < TileIndex)
{
return false;
}
else
{
// In some cases we'd like to know the position of the final byte of the last tile.
if (TileIndex == LineOrTileOffsetsPerLevel[Level].Num())
{
// If this is the last mip and the last tile that means that we don't have
if (LineOrTileOffsetsPerLevel.Num() > Level + 1)
{
OutBufferOffset = LineOrTileOffsetsPerLevel[Level + 1][0];
}
else
{
OutBufferOffset = FileLength;
}
}
else
{
OutBufferOffset = LineOrTileOffsetsPerLevel[Level][TileIndex];
}
return true;
}
}
bool FExrReader::CloseExrFile()
{
if (FileHandle == nullptr)
{
UE_LOG(LogExrReaderGpu, Error, TEXT("File is not open for reading. Please use OpenExrAndPrepareForPixelReading"));
return false;
}
fclose(FileHandle);
LineOrTileOffsetsPerLevel.Empty();
return true;
}
#endif