feat: decode IWI6 wavelet textures (#980)

* feat: decode IWI6 wavelet textures

* refactor: validate wavelet Huffman tables at compile time

* chore: small code style adjustments to IwiLoader

* chore: small code style adjustments to IwiWaveletDecoder

* fix: use shift right operator to better handle edge cases

* chore: add wavelet support for iwi8

* chore: add wavelet support for iwi13

---------

Co-authored-by: Jan Laupetin <[email protected]>
This commit is contained in:
mo
2026-09-06 23:08:20 +02:00
committed by GitHub
co-authored by Jan Laupetin
parent 2ca512abe7
commit 39e46495b9
4 changed files with 883 additions and 128 deletions
+198 -128
View File
@@ -1,6 +1,7 @@
#include "IwiLoader.h" #include "IwiLoader.h"
#include "Image/IwiTypes.h" #include "Image/IwiTypes.h"
#include "Image/IwiWaveletDecoder.h"
#include "Utils/Logging/Log.h" #include "Utils/Logging/Log.h"
#include <cassert> #include <cassert>
@@ -11,6 +12,25 @@ using namespace image;
namespace namespace
{ {
std::optional<IwiWaveletFormat> GetWaveletFormat6(const int8_t format)
{
switch (static_cast<iwi6::IwiFormat>(format))
{
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGBA:
return IwiWaveletFormat::RGBA;
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGB:
return IwiWaveletFormat::RGB;
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
return IwiWaveletFormat::LUMINANCE_ALPHA;
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
return IwiWaveletFormat::LUMINANCE;
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
return IwiWaveletFormat::ALPHA;
default:
return std::nullopt;
}
}
const ImageFormat* GetFormat6(int8_t format) const ImageFormat* GetFormat6(int8_t format)
{ {
switch (static_cast<iwi6::IwiFormat>(format)) switch (static_cast<iwi6::IwiFormat>(format))
@@ -33,13 +53,6 @@ namespace
return &format::R8_A8; return &format::R8_A8;
case iwi6::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE: case iwi6::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE:
return &format::R8; return &format::R8;
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGBA: // used
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGB: // used
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
case iwi6::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
con::error("Unsupported IWI format: {}", format);
break;
default: default:
con::error("Unknown IWI format: {}", format); con::error("Unknown IWI format: {}", format);
break; break;
@@ -59,54 +72,66 @@ namespace
return std::nullopt; return std::nullopt;
} }
const auto* format = GetFormat6(header.format); const auto width = header.dimensions[0];
if (format == nullptr) const auto height = header.dimensions[1];
return std::nullopt; const auto depth = header.dimensions[2];
const auto hasMipMaps = !(header.flags & iwi6::IwiFlags::IMG_FLAG_NOMIPMAPS);
auto width = header.dimensions[0]; TextureType textureType;
auto height = header.dimensions[1]; if (header.flags & iwi6::IwiFlags::IMG_FLAG_CUBEMAP)
auto depth = header.dimensions[2]; textureType = TextureType::T_CUBE;
auto hasMipMaps = !(header.flags & iwi6::IwiFlags::IMG_FLAG_NOMIPMAPS); else if (header.flags & iwi6::IwiFlags::IMG_FLAG_VOLMAP)
textureType = TextureType::T_3D;
else
textureType = TextureType::T_2D;
std::unique_ptr<Texture> texture; std::unique_ptr<Texture> texture;
if (header.flags & iwi6::IwiFlags::IMG_FLAG_CUBEMAP) if (const auto waveletFormat = GetWaveletFormat6(header.format))
texture = std::make_unique<TextureCube>(format, width, height, hasMipMaps);
else if (header.flags & iwi6::IwiFlags::IMG_FLAG_VOLMAP)
texture = std::make_unique<Texture3D>(format, width, height, depth, hasMipMaps);
else
texture = std::make_unique<Texture2D>(format, width, height, hasMipMaps);
texture->Allocate();
auto currentFileSize = sizeof(iwi6::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{ {
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount(); texture = DecodeIwiWavelet(stream, textureType, width, height, hasMipMaps, *waveletFormat);
currentFileSize += sizeOfMipLevel; if (!texture)
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi6::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt; return std::nullopt;
} }
else
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel); {
if (stream.gcount() != sizeOfMipLevel) const auto* format = GetFormat6(header.format);
{ if (format == nullptr)
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt; return std::nullopt;
texture = Texture::CreateForType(textureType, format, width, height, depth, hasMipMaps);
texture->Allocate();
auto currentFileSize = sizeof(iwi6::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount();
currentFileSize += sizeOfMipLevel;
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi6::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt;
}
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel);
if (stream.gcount() != sizeOfMipLevel)
{
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt;
}
} }
} }
CommonIwiMetaData meta{ const CommonIwiMetaData meta{
.m_no_picmip = (header.flags & iwi6::IwiFlags::IMG_FLAG_NOPICMIP) != 0, .m_no_picmip = (header.flags & iwi6::IwiFlags::IMG_FLAG_NOPICMIP) != 0,
.m_streaming = (header.flags & iwi6::IwiFlags::IMG_FLAG_STREAMING) != 0, .m_streaming = (header.flags & iwi6::IwiFlags::IMG_FLAG_STREAMING) != 0,
.m_clamp_u = (header.flags & iwi6::IwiFlags::IMG_FLAG_CLAMP_U) != 0, .m_clamp_u = (header.flags & iwi6::IwiFlags::IMG_FLAG_CLAMP_U) != 0,
.m_clamp_v = (header.flags & iwi6::IwiFlags::IMG_FLAG_CLAMP_V) != 0, .m_clamp_v = (header.flags & iwi6::IwiFlags::IMG_FLAG_CLAMP_V) != 0,
.m_dynamic = (header.flags & iwi6::IwiFlags::IMG_FLAG_DYNAMIC) != 0, .m_dynamic = (header.flags & iwi6::IwiFlags::IMG_FLAG_DYNAMIC) != 0,
.m_gamma = 0,
}; };
return IwiLoaderResult{ return IwiLoaderResult{
@@ -116,6 +141,25 @@ namespace
}; };
} }
std::optional<IwiWaveletFormat> GetWaveletFormat8(const int8_t format)
{
switch (static_cast<iwi8::IwiFormat>(format))
{
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_RGBA:
return IwiWaveletFormat::RGBA;
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_RGB:
return IwiWaveletFormat::RGB;
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
return IwiWaveletFormat::LUMINANCE_ALPHA;
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
return IwiWaveletFormat::LUMINANCE;
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
return IwiWaveletFormat::ALPHA;
default:
return std::nullopt;
}
}
const ImageFormat* GetFormat8(int8_t format) const ImageFormat* GetFormat8(int8_t format)
{ {
switch (static_cast<iwi8::IwiFormat>(format)) switch (static_cast<iwi8::IwiFormat>(format))
@@ -138,11 +182,6 @@ namespace
return &format::R8_A8; return &format::R8_A8;
case iwi8::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE: case iwi8::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE:
return &format::R8; return &format::R8;
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_RGBA: // used
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_RGB: // used
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
case iwi8::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
case iwi8::IwiFormat::IMG_FORMAT_DXT3A_AS_LUMINANCE: case iwi8::IwiFormat::IMG_FORMAT_DXT3A_AS_LUMINANCE:
case iwi8::IwiFormat::IMG_FORMAT_DXT5A_AS_LUMINANCE: case iwi8::IwiFormat::IMG_FORMAT_DXT5A_AS_LUMINANCE:
case iwi8::IwiFormat::IMG_FORMAT_DXT3A_AS_ALPHA: case iwi8::IwiFormat::IMG_FORMAT_DXT3A_AS_ALPHA:
@@ -172,28 +211,18 @@ namespace
return std::nullopt; return std::nullopt;
} }
const auto* format = GetFormat8(header.format); const auto width = header.dimensions[0];
if (format == nullptr) const auto height = header.dimensions[1];
return std::nullopt; const auto depth = header.dimensions[2];
const auto hasMipMaps = !(header.flags & iwi8::IwiFlags::IMG_FLAG_NOMIPMAPS);
auto width = header.dimensions[0]; TextureType textureType;
auto height = header.dimensions[1];
auto depth = header.dimensions[2];
auto hasMipMaps = !(header.flags & iwi8::IwiFlags::IMG_FLAG_NOMIPMAPS);
std::unique_ptr<Texture> texture;
if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_CUBE) if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_CUBE)
{ textureType = TextureType::T_CUBE;
texture = std::make_unique<TextureCube>(format, width, height, hasMipMaps);
}
else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_3D) else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_3D)
{ textureType = TextureType::T_3D;
texture = std::make_unique<Texture3D>(format, width, height, depth, hasMipMaps);
}
else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_2D) else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_2D)
{ textureType = TextureType::T_2D;
texture = std::make_unique<Texture2D>(format, width, height, hasMipMaps);
}
else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_1D) else if ((header.flags & iwi8::IwiFlags::IMG_FLAG_MAPTYPE_MASK) == iwi8::IwiFlags::IMG_FLAG_MAPTYPE_1D)
{ {
con::error("Iwi has unsupported map type 1D"); con::error("Iwi has unsupported map type 1D");
@@ -205,37 +234,53 @@ namespace
return std::nullopt; return std::nullopt;
} }
texture->Allocate(); std::unique_ptr<Texture> texture;
if (const auto waveletFormat = GetWaveletFormat8(header.format))
auto currentFileSize = sizeof(iwi8::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{ {
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount(); texture = DecodeIwiWavelet(stream, textureType, width, height, hasMipMaps, *waveletFormat);
currentFileSize += sizeOfMipLevel; if (!texture)
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi8::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt; return std::nullopt;
} }
else
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel); {
if (stream.gcount() != sizeOfMipLevel) const auto* format = GetFormat8(header.format);
{ if (format == nullptr)
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt; return std::nullopt;
texture = Texture::CreateForType(textureType, format, width, height, depth, hasMipMaps);
texture->Allocate();
auto currentFileSize = sizeof(iwi8::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount();
currentFileSize += sizeOfMipLevel;
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi8::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt;
}
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel);
if (stream.gcount() != sizeOfMipLevel)
{
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt;
}
} }
} }
CommonIwiMetaData meta{ const CommonIwiMetaData meta{
.m_no_picmip = (header.flags & iwi8::IwiFlags::IMG_FLAG_NOPICMIP) != 0, .m_no_picmip = (header.flags & iwi8::IwiFlags::IMG_FLAG_NOPICMIP) != 0,
.m_streaming = (header.flags & iwi8::IwiFlags::IMG_FLAG_STREAMING) != 0, .m_streaming = (header.flags & iwi8::IwiFlags::IMG_FLAG_STREAMING) != 0,
.m_clamp_u = (header.flags & iwi8::IwiFlags::IMG_FLAG_CLAMP_U) != 0, .m_clamp_u = (header.flags & iwi8::IwiFlags::IMG_FLAG_CLAMP_U) != 0,
.m_clamp_v = (header.flags & iwi8::IwiFlags::IMG_FLAG_CLAMP_V) != 0, .m_clamp_v = (header.flags & iwi8::IwiFlags::IMG_FLAG_CLAMP_V) != 0,
.m_dynamic = (header.flags & iwi8::IwiFlags::IMG_FLAG_DYNAMIC) != 0, .m_dynamic = (header.flags & iwi8::IwiFlags::IMG_FLAG_DYNAMIC) != 0,
.m_gamma = 0,
}; };
return IwiLoaderResult{ return IwiLoaderResult{
@@ -245,6 +290,25 @@ namespace
}; };
} }
std::optional<IwiWaveletFormat> GetWaveletFormat13(const int8_t format)
{
switch (static_cast<iwi13::IwiFormat>(format))
{
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_RGBA:
return IwiWaveletFormat::RGBA;
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_RGB:
return IwiWaveletFormat::RGB;
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
return IwiWaveletFormat::LUMINANCE_ALPHA;
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
return IwiWaveletFormat::LUMINANCE;
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
return IwiWaveletFormat::ALPHA;
default:
return std::nullopt;
}
}
const ImageFormat* GetFormat13(int8_t format) const ImageFormat* GetFormat13(int8_t format)
{ {
switch (static_cast<iwi13::IwiFormat>(format)) switch (static_cast<iwi13::IwiFormat>(format))
@@ -267,11 +331,6 @@ namespace
return &format::R8_A8; return &format::R8_A8;
case iwi13::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE: case iwi13::IwiFormat::IMG_FORMAT_BITMAP_LUMINANCE:
return &format::R8; return &format::R8;
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_RGBA: // used
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_RGB: // used
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE_ALPHA:
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE:
case iwi13::IwiFormat::IMG_FORMAT_WAVELET_ALPHA:
case iwi13::IwiFormat::IMG_FORMAT_BITMAP_RGB565: case iwi13::IwiFormat::IMG_FORMAT_BITMAP_RGB565:
case iwi13::IwiFormat::IMG_FORMAT_BITMAP_RGB5A3: case iwi13::IwiFormat::IMG_FORMAT_BITMAP_RGB5A3:
case iwi13::IwiFormat::IMG_FORMAT_BITMAP_C8: case iwi13::IwiFormat::IMG_FORMAT_BITMAP_C8:
@@ -298,49 +357,60 @@ namespace
return std::nullopt; return std::nullopt;
} }
const auto* format = GetFormat13(header.format); const auto width = header.dimensions[0];
if (format == nullptr) const auto height = header.dimensions[1];
return std::nullopt; const auto depth = header.dimensions[2];
const auto hasMipMaps = !(header.flags & iwi13::IwiFlags::IMG_FLAG_NOMIPMAPS);
auto width = header.dimensions[0]; TextureType textureType;
auto height = header.dimensions[1]; if (header.flags & iwi13::IwiFlags::IMG_FLAG_CUBEMAP)
auto depth = header.dimensions[2]; textureType = TextureType::T_CUBE;
auto hasMipMaps = !(header.flags & iwi13::IwiFlags::IMG_FLAG_NOMIPMAPS); else if (header.flags & iwi13::IwiFlags::IMG_FLAG_VOLMAP)
textureType = TextureType::T_3D;
else
textureType = TextureType::T_2D;
std::unique_ptr<Texture> texture; std::unique_ptr<Texture> texture;
if (header.flags & iwi13::IwiFlags::IMG_FLAG_CUBEMAP) if (const auto waveletFormat = GetWaveletFormat13(header.format))
texture = std::make_unique<TextureCube>(format, width, height, hasMipMaps);
else if (header.flags & iwi13::IwiFlags::IMG_FLAG_VOLMAP)
texture = std::make_unique<Texture3D>(format, width, height, depth, hasMipMaps);
else
texture = std::make_unique<Texture2D>(format, width, height, hasMipMaps);
texture->Allocate();
auto currentFileSize = sizeof(iwi13::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{ {
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount(); texture = DecodeIwiWavelet(stream, textureType, width, height, hasMipMaps, *waveletFormat);
currentFileSize += sizeOfMipLevel; if (!texture)
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi13::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt; return std::nullopt;
} }
else
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel); {
if (stream.gcount() != sizeOfMipLevel) const auto* format = GetFormat13(header.format);
{ if (format == nullptr)
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt; return std::nullopt;
texture = Texture::CreateForType(textureType, format, width, height, depth, hasMipMaps);
texture->Allocate();
auto currentFileSize = sizeof(iwi13::IwiHeader) + sizeof(IwiVersionHeader);
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto currentMipLevel = mipMapCount - 1; currentMipLevel >= 0; currentMipLevel--)
{
const auto sizeOfMipLevel = texture->GetSizeOfMipLevel(currentMipLevel) * texture->GetFaceCount();
currentFileSize += sizeOfMipLevel;
if (currentMipLevel < static_cast<int>(std::extent_v<decltype(iwi13::IwiHeader::fileSizeForPicmip)>)
&& currentFileSize != header.fileSizeForPicmip[currentMipLevel])
{
con::error("Iwi has invalid file size for picmip {}", currentMipLevel);
return std::nullopt;
}
stream.read(reinterpret_cast<char*>(texture->GetBufferForMipLevel(currentMipLevel)), sizeOfMipLevel);
if (stream.gcount() != sizeOfMipLevel)
{
con::error("Unexpected eof of iwi in mip level {}", currentMipLevel);
return std::nullopt;
}
} }
} }
CommonIwiMetaData meta{ const CommonIwiMetaData meta{
.m_no_picmip = (header.flags & iwi13::IwiFlags::IMG_FLAG_NOPICMIP) != 0, .m_no_picmip = (header.flags & iwi13::IwiFlags::IMG_FLAG_NOPICMIP) != 0,
.m_streaming = (header.flags & iwi13::IwiFlags::IMG_FLAG_STREAMING) != 0, .m_streaming = (header.flags & iwi13::IwiFlags::IMG_FLAG_STREAMING) != 0,
.m_clamp_u = (header.flags & iwi13::IwiFlags::IMG_FLAG_CLAMP_U) != 0, .m_clamp_u = (header.flags & iwi13::IwiFlags::IMG_FLAG_CLAMP_U) != 0,
@@ -415,10 +485,10 @@ namespace
if (format == nullptr) if (format == nullptr)
return std::nullopt; return std::nullopt;
auto width = header.dimensions[0]; const auto width = header.dimensions[0];
auto height = header.dimensions[1]; const auto height = header.dimensions[1];
auto depth = header.dimensions[2]; const auto depth = header.dimensions[2];
auto hasMipMaps = !(header.flags & iwi27::IwiFlags::IMG_FLAG_NOMIPMAPS); const auto hasMipMaps = !(header.flags & iwi27::IwiFlags::IMG_FLAG_NOMIPMAPS);
std::unique_ptr<Texture> texture; std::unique_ptr<Texture> texture;
if (header.flags & iwi27::IwiFlags::IMG_FLAG_CUBEMAP) if (header.flags & iwi27::IwiFlags::IMG_FLAG_CUBEMAP)
@@ -453,7 +523,7 @@ namespace
} }
} }
CommonIwiMetaData meta{ const CommonIwiMetaData meta{
.m_no_picmip = (header.flags & iwi27::IwiFlags::IMG_FLAG_NOPICMIP) != 0, .m_no_picmip = (header.flags & iwi27::IwiFlags::IMG_FLAG_NOPICMIP) != 0,
.m_streaming = (header.flags & iwi27::IwiFlags::IMG_FLAG_STREAMING) != 0, .m_streaming = (header.flags & iwi27::IwiFlags::IMG_FLAG_STREAMING) != 0,
.m_clamp_u = (header.flags & iwi27::IwiFlags::IMG_FLAG_CLAMP_U) != 0, .m_clamp_u = (header.flags & iwi27::IwiFlags::IMG_FLAG_CLAMP_U) != 0,
+474
View File
@@ -0,0 +1,474 @@
#include "IwiWaveletDecoder.h"
#include "Image/ImageFormat.h"
#include "Utils/Logging/Log.h"
#include <algorithm>
#include <array>
#include <bit>
#include <cassert>
#include <cstdint>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>
namespace
{
constexpr auto HUFFMAN_LOOKUP_BITS = 12u;
constexpr auto HUFFMAN_LOOKUP_SIZE = 1u << HUFFMAN_LOOKUP_BITS;
constexpr auto ESCAPE_VALUE = std::numeric_limits<std::int16_t>::min();
struct HuffmanCodeword
{
std::uint16_t code;
std::uint8_t bit_count;
std::int16_t value;
};
struct HuffmanDecode
{
std::int16_t value;
std::uint8_t bit_count;
};
// These are the unique, LSB-first codewords from the IW wavelet decoder's
// three 4096-entry lookup tables. The full lookup tables are generated at
// compile time below.
// clang-format off
constexpr auto BLUE_CODEWORDS = std::to_array<HuffmanCodeword>({
{0x001, 3, 0}, {0x004, 5, 4}, {0x005, 5, 2}, {0x007, 5, 1}, {0x00A, 5, 3}, {0x014, 5, -4},
{0x015, 5, -2}, {0x017, 5, -1}, {0x01A, 5, -3}, {0x000, 6, 12}, {0x002, 6, 10}, {0x003, 6, 7},
{0x006, 6, 9}, {0x00B, 6, 6}, {0x018, 6, 11}, {0x01E, 6, 8}, {0x01F, 6, 5}, {0x020, 6, -12},
{0x022, 6, -10}, {0x023, 6, -7}, {0x026, 6, -9}, {0x02B, 6, -6}, {0x038, 6, -11}, {0x03C, 6, ESCAPE_VALUE},
{0x03E, 6, -8}, {0x03F, 6, -5}, {0x00F, 7, 13}, {0x012, 7, 19}, {0x016, 7, 18}, {0x01B, 7, 14},
{0x028, 7, 21}, {0x02C, 7, 20}, {0x02D, 7, 16}, {0x02E, 7, 17}, {0x030, 7, 22}, {0x03D, 7, 15},
{0x04F, 7, -13}, {0x052, 7, -19}, {0x056, 7, -18}, {0x05B, 7, -14}, {0x068, 7, -21}, {0x06C, 7, -20},
{0x06D, 7, -16}, {0x06E, 7, -17}, {0x070, 7, -22}, {0x07D, 7, -15}, {0x008, 8, 34}, {0x00D, 8, 28},
{0x00E, 8, 29}, {0x013, 8, 26}, {0x01D, 8, 27}, {0x02F, 8, 23}, {0x033, 8, 25}, {0x03B, 8, 24},
{0x048, 8, 33}, {0x04C, 8, 32}, {0x05C, 8, 31}, {0x072, 8, 30}, {0x088, 8, -34}, {0x08D, 8, -28},
{0x08E, 8, -29}, {0x093, 8, -26}, {0x09D, 8, -27}, {0x0AF, 8, -23}, {0x0B3, 8, -25}, {0x0BB, 8, -24},
{0x0C8, 8, -33}, {0x0CC, 8, -32}, {0x0DC, 8, -31}, {0x0F2, 8, -30}, {0x00C, 9, 47}, {0x01C, 9, 46},
{0x032, 9, 45}, {0x036, 9, 44}, {0x050, 9, 48}, {0x076, 9, 43}, {0x07B, 9, 37}, {0x090, 9, 49},
{0x0CD, 9, 40}, {0x0CE, 9, 41}, {0x0D3, 9, 38}, {0x0DD, 9, 39}, {0x0EF, 9, 35}, {0x0F6, 9, 42},
{0x0FB, 9, 36}, {0x10C, 9, -47}, {0x11C, 9, -46}, {0x132, 9, -45}, {0x136, 9, -44}, {0x150, 9, -48},
{0x176, 9, -43}, {0x17B, 9, -37}, {0x190, 9, -49}, {0x1CD, 9, -40}, {0x1CE, 9, -41}, {0x1D3, 9, -38},
{0x1DD, 9, -39}, {0x1EF, 9, -35}, {0x1F6, 9, -42}, {0x1FB, 9, -36}, {0x010, 10, 65}, {0x04D, 10, 56},
{0x04E, 10, 57}, {0x05D, 10, 55}, {0x08C, 10, 62}, {0x09C, 10, 61}, {0x110, 10, 64}, {0x153, 10, 53},
{0x15D, 10, 54}, {0x16F, 10, 50}, {0x173, 10, 52}, {0x19C, 10, 60}, {0x1B2, 10, 59}, {0x1B6, 10, 58},
{0x1D0, 10, 63}, {0x1F3, 10, 51}, {0x210, 10, -65}, {0x24D, 10, -56}, {0x24E, 10, -57}, {0x25D, 10, -55},
{0x28C, 10, -62}, {0x29C, 10, -61}, {0x310, 10, -64}, {0x353, 10, -53}, {0x35D, 10, -54}, {0x36F, 10, -50},
{0x373, 10, -52}, {0x39C, 10, -60}, {0x3B2, 10, -59}, {0x3B6, 10, -58}, {0x3D0, 10, -63}, {0x3F3, 10, -51},
{0x053, 11, 70}, {0x06F, 11, 66}, {0x073, 11, 69}, {0x0B2, 11, 77}, {0x0B6, 11, 75}, {0x0D0, 11, 81},
{0x14E, 11, 73}, {0x18C, 11, 79}, {0x273, 11, 68}, {0x2B2, 11, 76}, {0x2B6, 11, 74}, {0x2D0, 11, 80},
{0x2F3, 11, 67}, {0x34D, 11, 71}, {0x34E, 11, 72}, {0x38C, 11, 78}, {0x453, 11, -70}, {0x46F, 11, -66},
{0x473, 11, -69}, {0x4B2, 11, -77}, {0x4B6, 11, -75}, {0x4D0, 11, -81}, {0x54E, 11, -73}, {0x58C, 11, -79},
{0x673, 11, -68}, {0x6B2, 11, -76}, {0x6B6, 11, -74}, {0x6D0, 11, -80}, {0x6F3, 11, -67}, {0x74D, 11, -71},
{0x74E, 11, -72}, {0x78C, 11, -78}, {0x0F3, 12, 85}, {0x14D, 12, 89}, {0x253, 12, 87}, {0x26F, 12, 83},
{0x4F3, 12, 84}, {0x54D, 12, 88}, {0x653, 12, 86}, {0x66F, 12, 82}, {0x8F3, 12, -85}, {0x94D, 12, -89},
{0xA53, 12, -87}, {0xA6F, 12, -83}, {0xCF3, 12, -84}, {0xD4D, 12, -88}, {0xE53, 12, -86}, {0xE6F, 12, -82},
});
static_assert(BLUE_CODEWORDS.size() == 180);
constexpr auto RED_GREEN_CODEWORDS = std::to_array<HuffmanCodeword>({
{0x003, 2, 0}, {0x002, 3, 1}, {0x006, 3, -1}, {0x001, 4, 2}, {0x009, 4, -2}, {0x004, 5, 4},
{0x00D, 5, 3}, {0x014, 5, -4}, {0x01D, 5, -3}, {0x00C, 6, 6}, {0x010, 6, 7}, {0x015, 6, 5},
{0x02C, 6, -6}, {0x030, 6, -7}, {0x035, 6, -5}, {0x018, 7, 10}, {0x01C, 7, 9}, {0x020, 7, 11},
{0x025, 7, 8}, {0x058, 7, -10}, {0x05C, 7, -9}, {0x060, 7, -11}, {0x065, 7, -8}, {0x068, 7, ESCAPE_VALUE},
{0x038, 8, 14}, {0x040, 8, 16}, {0x045, 8, 12}, {0x048, 8, 15}, {0x07C, 8, 13}, {0x0B8, 8, -14},
{0x0C0, 8, -16}, {0x0C5, 8, -12}, {0x0C8, 8, -15}, {0x0FC, 8, -13}, {0x080, 9, 22}, {0x085, 9, 17},
{0x088, 9, 21}, {0x0A8, 9, 20}, {0x0BC, 9, 18}, {0x0F8, 9, 19}, {0x180, 9, -22}, {0x185, 9, -17},
{0x188, 9, -21}, {0x1A8, 9, -20}, {0x1BC, 9, -18}, {0x1F8, 9, -19}, {0x000, 10, 30}, {0x03C, 10, 25},
{0x078, 10, 26}, {0x100, 10, 29}, {0x105, 10, 23}, {0x108, 10, 28}, {0x128, 10, 27}, {0x13C, 10, 24},
{0x200, 10, -30}, {0x23C, 10, -25}, {0x278, 10, -26}, {0x300, 10, -29}, {0x305, 10, -23}, {0x308, 10, -28},
{0x328, 10, -27}, {0x33C, 10, -24}, {0x005, 11, 31}, {0x008, 11, 37}, {0x028, 11, 35}, {0x178, 11, 33},
{0x208, 11, 36}, {0x228, 11, 34}, {0x378, 11, 32}, {0x405, 11, -31}, {0x408, 11, -37}, {0x428, 11, -35},
{0x578, 11, -33}, {0x608, 11, -36}, {0x628, 11, -34}, {0x778, 11, -32}, {0x205, 12, 39}, {0x605, 12, 38},
{0xA05, 12, -39}, {0xE05, 12, -38},
});
static_assert(RED_GREEN_CODEWORDS.size() == 80);
constexpr auto ALPHA_CODEWORDS = std::to_array<HuffmanCodeword>({
{0x001, 1, 0}, {0x000, 4, ESCAPE_VALUE}, {0x002, 4, 1}, {0x00A, 4, -1}, {0x00C, 5, 2}, {0x01C, 5, -2}, {0x016, 6, 3},
{0x018, 6, 4}, {0x036, 6, -3}, {0x038, 6, -4}, {0x004, 7, 7}, {0x02E, 7, 5}, {0x034, 7, 6}, {0x044, 7, -7},
{0x06E, 7, -5}, {0x074, 7, -6}, {0x006, 8, 11}, {0x008, 8, 14}, {0x014, 8, 12}, {0x01E, 8, 9}, {0x048, 8, 15},
{0x066, 8, 10}, {0x068, 8, 13}, {0x07E, 8, 8}, {0x086, 8, -11}, {0x088, 8, -14}, {0x094, 8, -12}, {0x09E, 8, -9},
{0x0C8, 8, -15}, {0x0E6, 8, -10}, {0x0E8, 8, -13}, {0x0FE, 8, -8}, {0x028, 9, 23}, {0x046, 9, 19}, {0x054, 9, 20},
{0x08E, 9, 17}, {0x0A4, 9, 22}, {0x0A8, 9, 24}, {0x0C6, 9, 18}, {0x0DE, 9, 16}, {0x0E4, 9, 21}, {0x128, 9, -23},
{0x146, 9, -19}, {0x154, 9, -20}, {0x18E, 9, -17}, {0x1A4, 9, -22}, {0x1A8, 9, -24}, {0x1C6, 9, -18}, {0x1DE, 9, -16},
{0x1E4, 9, -21}, {0x00E, 10, 29}, {0x024, 10, 37}, {0x026, 10, 31}, {0x04E, 10, 28}, {0x064, 10, 35}, {0x0BE, 10, 32},
{0x0D4, 10, 33}, {0x124, 10, 36}, {0x126, 10, 30}, {0x13E, 10, 25}, {0x15E, 10, 26}, {0x164, 10, 34}, {0x1A6, 10, 127},
{0x1CE, 10, 27}, {0x1D4, 10, 128}, {0x20E, 10, -29}, {0x224, 10, -37}, {0x226, 10, -31}, {0x24E, 10, -28}, {0x264, 10, -35},
{0x2BE, 10, -32}, {0x2D4, 10, -33}, {0x324, 10, -36}, {0x326, 10, -30}, {0x33E, 10, -25}, {0x35E, 10, -26}, {0x364, 10, -34},
{0x3A6, 10, -127}, {0x3CE, 10, -27}, {0x3D4, 10, -128}, {0x03E, 11, 41}, {0x05E, 11, 43}, {0x0A6, 11, 50}, {0x0CE, 11, 48},
{0x10E, 11, 49}, {0x14E, 11, 64}, {0x1BE, 11, 39}, {0x23E, 11, 40}, {0x25E, 11, 42}, {0x2A6, 11, 47}, {0x2CE, 11, 44},
{0x30E, 11, 46}, {0x34E, 11, 45}, {0x3BE, 11, 38}, {0x43E, 11, -41}, {0x45E, 11, -43}, {0x4A6, 11, -50}, {0x4CE, 11, -48},
{0x50E, 11, -49}, {0x54E, 11, -64}, {0x5BE, 11, -39}, {0x63E, 11, -40}, {0x65E, 11, -42}, {0x6A6, 11, -47}, {0x6CE, 11, -44},
{0x70E, 11, -46}, {0x74E, 11, -45}, {0x7BE, 11, -38},
});
static_assert(ALPHA_CODEWORDS.size() == 108);
// clang-format on
template<std::size_t CodewordCount>
consteval std::array<HuffmanDecode, HUFFMAN_LOOKUP_SIZE> CreateHuffmanLookup(const std::array<HuffmanCodeword, CodewordCount>& codewords)
{
std::array<HuffmanDecode, HUFFMAN_LOOKUP_SIZE> result{};
for (const auto codeword : codewords)
{
if (codeword.bit_count == 0 || codeword.bit_count > HUFFMAN_LOOKUP_BITS)
throw std::runtime_error("Invalid Huffman codeword bit count");
const auto step = 1u << codeword.bit_count;
if (codeword.code >= step)
throw std::runtime_error("Huffman codeword does not fit its bit count");
for (auto index = static_cast<unsigned>(codeword.code); index < result.size(); index += step)
{
if (result[index].bit_count != 0)
throw std::runtime_error("Overlapping Huffman codewords");
result[index] = HuffmanDecode{.value = codeword.value, .bit_count = codeword.bit_count};
}
}
for (const auto entry : result)
{
if (entry.bit_count == 0)
throw std::runtime_error("Incomplete Huffman lookup");
}
return result;
}
constexpr auto BLUE_LOOKUP = CreateHuffmanLookup(BLUE_CODEWORDS);
constexpr auto RED_GREEN_LOOKUP = CreateHuffmanLookup(RED_GREEN_CODEWORDS);
constexpr auto ALPHA_LOOKUP = CreateHuffmanLookup(ALPHA_CODEWORDS);
class WaveletBitReader
{
public:
explicit WaveletBitReader(std::vector<std::uint8_t> data)
: m_data(std::move(data))
{
}
bool ReadRawByte(std::uint8_t& value)
{
if (m_bits_started || m_byte_offset >= m_data.size())
return false;
value = m_data[m_byte_offset++];
return true;
}
bool ReadBits(const unsigned bitCount, unsigned& value)
{
StartBits();
if (bitCount > 32 || m_bit_offset + bitCount > m_data.size() * 8u)
return false;
value = PeekBits(bitCount);
m_bit_offset += bitCount;
return true;
}
unsigned PeekBits(const unsigned bitCount)
{
StartBits();
unsigned result = 0;
const auto availableBitCount = m_data.size() * 8u;
for (auto bitIndex = 0u; bitIndex < bitCount && m_bit_offset + bitIndex < availableBitCount; bitIndex++)
{
const auto sourceBit = m_bit_offset + bitIndex;
result |= ((m_data[sourceBit / 8u] >> (sourceBit % 8u)) & 1u) << bitIndex;
}
return result;
}
private:
void StartBits()
{
if (!m_bits_started)
{
m_bit_offset = m_byte_offset * 8u;
m_bits_started = true;
}
}
std::vector<std::uint8_t> m_data;
std::size_t m_byte_offset = 0;
std::size_t m_bit_offset = 0;
bool m_bits_started = false;
};
struct WaveletLayout
{
const image::ImageFormat* output_format;
unsigned channel_count;
unsigned bytes_per_pixel;
};
WaveletLayout GetLayout(const image::IwiWaveletFormat format)
{
switch (format)
{
case image::IwiWaveletFormat::RGBA:
return {.output_format = &image::format::B8_G8_R8_A8, .channel_count = 4, .bytes_per_pixel = 4};
case image::IwiWaveletFormat::RGB:
return {.output_format = &image::format::B8_G8_R8_X8, .channel_count = 3, .bytes_per_pixel = 4};
case image::IwiWaveletFormat::LUMINANCE_ALPHA:
return {.output_format = &image::format::R8_A8, .channel_count = 2, .bytes_per_pixel = 2};
case image::IwiWaveletFormat::LUMINANCE:
return {.output_format = &image::format::R8, .channel_count = 1, .bytes_per_pixel = 1};
case image::IwiWaveletFormat::ALPHA:
return {.output_format = &image::format::A8, .channel_count = 1, .bytes_per_pixel = 1};
}
assert(false);
con::error("Unexpected wavelet format {}", std::to_underlying(format));
return {};
}
bool DecodeValue(
const std::array<HuffmanDecode, HUFFMAN_LOOKUP_SIZE>& lookup, const unsigned escapeBitCount, const int escapeBias, WaveletBitReader& reader, int& value)
{
const auto entry = lookup[reader.PeekBits(HUFFMAN_LOOKUP_BITS)];
unsigned ignored;
if (!entry.bit_count || !reader.ReadBits(entry.bit_count, ignored))
return false;
value = entry.value;
if (entry.value == ESCAPE_VALUE)
{
unsigned escapedValue;
if (!reader.ReadBits(escapeBitCount, escapedValue))
return false;
value = static_cast<int>(escapedValue) - escapeBias;
}
return true;
}
bool DecodeCoefficients(const std::array<HuffmanDecode, HUFFMAN_LOOKUP_SIZE>& lookup,
const unsigned escapeBitCount,
const int escapeBias,
WaveletBitReader& reader,
std::array<int, 3>& coefficients)
{
for (auto& coefficient : coefficients)
{
if (!DecodeValue(lookup, escapeBitCount, escapeBias, reader, coefficient))
return false;
}
return true;
}
void ReconstructChannel(const std::uint8_t* source,
std::uint8_t* topLeft,
const unsigned bytesPerPixel,
const unsigned stride,
const unsigned channel,
const unsigned parity,
const std::array<int, 3>& coefficients)
{
const auto base = 2 * source[channel];
const auto horizontal = coefficients[0];
const auto vertical = coefficients[1];
const auto diagonal = coefficients[2];
topLeft[channel] = static_cast<std::uint8_t>(parity + ((diagonal + vertical + horizontal + base) >> 1));
topLeft[bytesPerPixel + channel] = static_cast<std::uint8_t>((horizontal + base - diagonal - vertical) >> 1);
topLeft[stride + channel] = static_cast<std::uint8_t>((vertical - diagonal + base - horizontal) >> 1);
topLeft[stride + bytesPerPixel + channel] = static_cast<std::uint8_t>((base - horizontal - vertical + diagonal) >> 1);
}
bool AddDeltaToMipmap(std::uint8_t* pixels, const unsigned pixelCount, const unsigned channelCount, const unsigned bytesPerPixel, WaveletBitReader& reader)
{
for (auto pixelIndex = 0u; pixelIndex < pixelCount; pixelIndex++)
{
for (auto channel = 0u; channel < channelCount; channel++)
{
int delta;
if (!DecodeValue(ALPHA_LOOKUP, 9, 255, reader, delta))
return false;
pixels[channel] = static_cast<std::uint8_t>(pixels[channel] + delta);
}
pixels += bytesPerPixel;
}
return true;
}
bool DecodeRawLevel(
std::uint8_t* destination, const unsigned pixelCount, const unsigned channelCount, const unsigned bytesPerPixel, WaveletBitReader& reader)
{
for (auto pixelIndex = 0u; pixelIndex < pixelCount; pixelIndex++)
{
for (auto channel = 0u; channel < channelCount; channel++)
{
if (!reader.ReadRawByte(destination[channel]))
return false;
}
for (auto channel = channelCount; channel < bytesPerPixel; channel++)
destination[channel] = 0xFF;
destination += bytesPerPixel;
}
return true;
}
bool DecodeWaveletLevel(const std::uint8_t* source,
std::uint8_t* destination,
const unsigned width,
const unsigned height,
const unsigned channelCount,
const unsigned bytesPerPixel,
WaveletBitReader& reader)
{
if (width <= 1 || height <= 1)
return DecodeRawLevel(destination, width * height, channelCount, bytesPerPixel, reader);
if (!source)
return false;
unsigned needsMipDelta;
if (!reader.ReadBits(1, needsMipDelta))
return false;
std::vector<std::uint8_t> adjustedSource;
if (needsMipDelta)
{
const auto sourceSize = width * height / 4u * bytesPerPixel;
adjustedSource.assign(source, source + sourceSize);
if (!AddDeltaToMipmap(adjustedSource.data(), width * height / 4u, channelCount, bytesPerPixel, reader))
return false;
source = adjustedSource.data();
}
const auto stride = width * bytesPerPixel;
for (auto y = 0u; y < height; y += 2)
{
for (auto x = 0u; x < width; x += 2)
{
const auto* sourcePixel = source + ((y / 2u) * (width / 2u) + x / 2u) * bytesPerPixel;
auto* destinationPixel = destination + (y * width + x) * bytesPerPixel;
std::array<int, 3> blueCoefficients{};
if (channelCount != 1)
{
unsigned parity;
if (!reader.ReadBits(1, parity) || !DecodeCoefficients(BLUE_LOOKUP, 9, 0xFF, reader, blueCoefficients))
return false;
ReconstructChannel(sourcePixel, destinationPixel, bytesPerPixel, stride, 0, parity, blueCoefficients);
if (channelCount >= 3)
{
for (auto channel = 1u; channel <= 2u; channel++)
{
std::array<int, 3> coefficients{};
if (!reader.ReadBits(1, parity) || !DecodeCoefficients(RED_GREEN_LOOKUP, 10, 0x1FE, reader, coefficients))
return false;
for (auto coefficientIndex = 0u; coefficientIndex < coefficients.size(); coefficientIndex++)
coefficients[coefficientIndex] += blueCoefficients[coefficientIndex];
ReconstructChannel(sourcePixel, destinationPixel, bytesPerPixel, stride, channel, parity, coefficients);
}
}
}
if (channelCount == 3)
{
destinationPixel[3] = 0xFF;
destinationPixel[bytesPerPixel + 3] = 0xFF;
destinationPixel[stride + 3] = 0xFF;
destinationPixel[stride + bytesPerPixel + 3] = 0xFF;
}
else
{
unsigned parity;
std::array<int, 3> coefficients{};
if (!reader.ReadBits(1, parity) || !DecodeCoefficients(ALPHA_LOOKUP, 9, 0xFF, reader, coefficients))
return false;
ReconstructChannel(sourcePixel, destinationPixel, bytesPerPixel, stride, channelCount - 1u, parity, coefficients);
}
}
}
return true;
}
} // namespace
namespace image
{
std::unique_ptr<Texture> DecodeIwiWavelet(
std::istream& stream, const TextureType textureType, const unsigned width, const unsigned height, const bool hasMipMaps, const IwiWaveletFormat format)
{
if (textureType == TextureType::T_3D)
{
con::error("IWI wavelet volume textures are unsupported");
return nullptr;
}
if (!width || !height || !std::has_single_bit(width) || !std::has_single_bit(height))
{
con::error("IWI wavelet dimensions must be powers of two");
return nullptr;
}
if (!hasMipMaps && width > 1 && height > 1)
{
con::error("IWI wavelet textures larger than 1x1 require mipmaps");
return nullptr;
}
const auto layout = GetLayout(format);
if (!layout.output_format)
return nullptr;
std::vector<std::uint8_t> compressedData{std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
if (stream.bad())
{
con::error("Failed to read IWI wavelet data");
return nullptr;
}
WaveletBitReader reader(std::move(compressedData));
auto texture = Texture::CreateForType(textureType, layout.output_format, width, height, 1, hasMipMaps);
texture->Allocate();
const auto mipMapCount = hasMipMaps ? texture->GetMipMapCount() : 1;
for (auto mipLevel = mipMapCount - 1; mipLevel >= 0; mipLevel--)
{
for (auto face = 0; face < texture->GetFaceCount(); face++)
{
const auto* source = mipLevel + 1 < mipMapCount ? texture->GetBufferForMipLevel(mipLevel + 1, face) : nullptr;
auto* destination = texture->GetBufferForMipLevel(mipLevel, face);
const auto mipWidth = std::max(1u, width >> mipLevel);
const auto mipHeight = std::max(1u, height >> mipLevel);
if (!DecodeWaveletLevel(source, destination, mipWidth, mipHeight, layout.channel_count, layout.bytes_per_pixel, reader))
{
con::error("Unexpected end of IWI wavelet data in mip level {}", mipLevel);
return nullptr;
}
}
}
return texture;
}
} // namespace image
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Image/Texture.h"
#include <istream>
#include <memory>
namespace image
{
enum class IwiWaveletFormat
{
RGBA,
RGB,
LUMINANCE_ALPHA,
LUMINANCE,
ALPHA,
};
std::unique_ptr<Texture>
DecodeIwiWavelet(std::istream& stream, TextureType textureType, unsigned width, unsigned height, bool hasMipMaps, IwiWaveletFormat format);
} // namespace image
@@ -0,0 +1,190 @@
#include "Image/IwiLoader.h"
#include "Image/IwiTypes.h"
#include <catch2/catch_test_macros.hpp>
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
using namespace image;
namespace
{
class BitWriter
{
public:
void WriteBits(const unsigned value, const unsigned bitCount)
{
for (auto bitIndex = 0u; bitIndex < bitCount; bitIndex++)
{
if (m_bit_count % 8u == 0)
m_data.emplace_back(0);
m_data.back() |= ((value >> bitIndex) & 1u) << (m_bit_count % 8u);
m_bit_count++;
}
}
[[nodiscard]] const std::vector<std::uint8_t>& Data() const
{
return m_data;
}
private:
std::vector<std::uint8_t> m_data;
unsigned m_bit_count = 0;
};
void AppendUint16(std::string& data, const std::uint16_t value)
{
data.push_back(static_cast<char>(value));
data.push_back(static_cast<char>(value >> 8u));
}
void AppendUint32(std::string& data, const std::uint32_t value)
{
data.push_back(static_cast<char>(value));
data.push_back(static_cast<char>(value >> 8u));
data.push_back(static_cast<char>(value >> 16u));
data.push_back(static_cast<char>(value >> 24u));
}
std::string MakeIwi6(const iwi6::IwiFormat format, const std::vector<std::uint8_t>& payload)
{
std::string data{'I', 'W', 'i', static_cast<char>(IwiVersion::IWI_6)};
data.push_back(static_cast<char>(format));
data.push_back(0);
AppendUint16(data, 2);
AppendUint16(data, 2);
AppendUint16(data, 1);
const auto fileSize = static_cast<std::uint32_t>(sizeof(IwiVersionHeader) + sizeof(iwi6::IwiHeader) + payload.size());
for (auto picmip = 0u; picmip < 4u; picmip++)
AppendUint32(data, fileSize);
data.append(reinterpret_cast<const char*>(payload.data()), payload.size());
return data;
}
std::optional<IwiLoaderResult> Load(const std::string& data)
{
std::istringstream stream(data);
return LoadIwi(stream);
}
TEST_CASE("IwiLoader: Can decode IWI6 wavelet luminance", "[image][iwi][wavelet]")
{
constexpr std::uint8_t PIXEL_VALUE = 0x40;
BitWriter bits;
bits.WriteBits(0, 1); // No predictor delta.
bits.WriteBits(0, 1); // Even reconstruction parity.
bits.WriteBits(0x001, 1); // Three zero alpha/luminance coefficients.
bits.WriteBits(0x001, 1);
bits.WriteBits(0x001, 1);
std::vector<std::uint8_t> payload{PIXEL_VALUE};
payload.insert(payload.end(), bits.Data().begin(), bits.Data().end());
auto result = Load(MakeIwi6(iwi6::IwiFormat::IMG_FORMAT_WAVELET_LUMINANCE, payload));
REQUIRE(result.has_value());
REQUIRE(result->m_version == IwiVersion::IWI_6);
REQUIRE(result->m_texture->GetFormat()->GetId() == ImageFormatId::R8);
REQUIRE(result->m_texture->GetWidth() == 2);
REQUIRE(result->m_texture->GetHeight() == 2);
REQUIRE(result->m_texture->GetMipMapCount() == 2);
const auto* largestMip = result->m_texture->GetBufferForMipLevel(0);
for (auto pixel = 0u; pixel < 4u; pixel++)
REQUIRE(largestMip[pixel] == PIXEL_VALUE);
REQUIRE(result->m_texture->GetBufferForMipLevel(1)[0] == PIXEL_VALUE);
}
TEST_CASE("IwiLoader: Can decode IWI6 wavelet RGBA", "[image][iwi][wavelet]")
{
constexpr std::uint8_t BLUE = 0x10;
constexpr std::uint8_t GREEN = 0x20;
constexpr std::uint8_t RED = 0x30;
constexpr std::uint8_t ALPHA = 0x40;
BitWriter bits;
bits.WriteBits(0, 1); // No predictor delta.
bits.WriteBits(0, 1); // Blue parity and zero coefficients.
bits.WriteBits(0x001, 3);
bits.WriteBits(0x001, 3);
bits.WriteBits(0x001, 3);
for (auto channel = 0u; channel < 2u; channel++)
{
bits.WriteBits(0, 1); // Red/green parity and zero residual coefficients.
bits.WriteBits(0x003, 2);
bits.WriteBits(0x003, 2);
bits.WriteBits(0x003, 2);
}
bits.WriteBits(0, 1); // Alpha parity and zero coefficients.
bits.WriteBits(0x001, 1);
bits.WriteBits(0x001, 1);
bits.WriteBits(0x001, 1);
std::vector<std::uint8_t> payload{BLUE, GREEN, RED, ALPHA};
payload.insert(payload.end(), bits.Data().begin(), bits.Data().end());
auto result = Load(MakeIwi6(iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGBA, payload));
REQUIRE(result.has_value());
REQUIRE(result->m_texture->GetFormat()->GetId() == ImageFormatId::B8_G8_R8_A8);
const auto* largestMip = result->m_texture->GetBufferForMipLevel(0);
for (auto pixel = 0u; pixel < 4u; pixel++)
{
REQUIRE(largestMip[pixel * 4u] == BLUE);
REQUIRE(largestMip[pixel * 4u + 1u] == GREEN);
REQUIRE(largestMip[pixel * 4u + 2u] == RED);
REQUIRE(largestMip[pixel * 4u + 3u] == ALPHA);
}
}
TEST_CASE("IwiLoader: Can decode IWI6 wavelet RGB", "[image][iwi][wavelet]")
{
constexpr std::uint8_t BLUE = 0x10;
constexpr std::uint8_t GREEN = 0x20;
constexpr std::uint8_t RED = 0x30;
BitWriter bits;
bits.WriteBits(0, 1); // No predictor delta.
bits.WriteBits(0, 1); // Blue parity and zero coefficients.
bits.WriteBits(0x001, 3);
bits.WriteBits(0x001, 3);
bits.WriteBits(0x001, 3);
for (auto channel = 0u; channel < 2u; channel++)
{
bits.WriteBits(0, 1); // Red/green parity and zero residual coefficients.
bits.WriteBits(0x003, 2);
bits.WriteBits(0x003, 2);
bits.WriteBits(0x003, 2);
}
std::vector<std::uint8_t> payload{BLUE, GREEN, RED};
payload.insert(payload.end(), bits.Data().begin(), bits.Data().end());
auto result = Load(MakeIwi6(iwi6::IwiFormat::IMG_FORMAT_WAVELET_RGB, payload));
REQUIRE(result.has_value());
REQUIRE(result->m_texture->GetFormat()->GetId() == ImageFormatId::B8_G8_R8_X8);
const auto* largestMip = result->m_texture->GetBufferForMipLevel(0);
for (auto pixel = 0u; pixel < 4u; pixel++)
{
REQUIRE(largestMip[pixel * 4u] == BLUE);
REQUIRE(largestMip[pixel * 4u + 1u] == GREEN);
REQUIRE(largestMip[pixel * 4u + 2u] == RED);
REQUIRE(largestMip[pixel * 4u + 3u] == 0xFF);
}
}
} // namespace