diff --git a/README.md b/README.md
index 461d8ee82..eed222f35 100644
--- a/README.md
+++ b/README.md
@@ -393,6 +393,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
✅ |
✅ |
+
+
+ |
+ XTF v5.0
+ |
+ ✅ |
+ ✅ |
+
(\*) These libraries are incomplete and still in development. Their interfaces are unstable and will likely change in the future.
diff --git a/docs/index.md b/docs/index.md
index 831f6f003..cc3090497 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -342,6 +342,13 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
✅ |
✅ |
+
+ |
+ XTF v5.0
+ |
+ ✅ |
+ ✅ |
+
\endhtmlonly
diff --git a/ext/bufferstream b/ext/bufferstream
index d468baf2b..20015dc22 160000
--- a/ext/bufferstream
+++ b/ext/bufferstream
@@ -1 +1 @@
-Subproject commit d468baf2bcdfb99cf7c426dd1565e6f23ac4c2fa
+Subproject commit 20015dc22ba7d2e6f440dede3378e95dca10256a
diff --git a/include/vtfpp/ImageFormats.h b/include/vtfpp/ImageFormats.h
index d707df474..22cdf847f 100644
--- a/include/vtfpp/ImageFormats.h
+++ b/include/vtfpp/ImageFormats.h
@@ -734,6 +734,20 @@ namespace ImageFormatDetails {
return length;
}
+// XTF (PLATFORM_XBOX) has padding between frames to align each one to 512 bytes
+[[nodiscard]] constexpr uint32_t getDataLengthXBOX(bool padded, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount = 1) {
+ uint32_t length = 0;
+ for (int j = 0; j < frameCount; j++) {
+ for (int i = 0; i < mipCount; i++) {
+ length += ImageFormatDetails::getDataLength(format, ImageDimensions::getMipDim(i, width), ImageDimensions::getMipDim(i, height), sliceCount) * faceCount;
+ }
+ if (padded && j + 1 != frameCount && length > 512) {
+ length += sourcepp::math::paddingForAlignment(512, length);
+ }
+ }
+ return length;
+}
+
[[nodiscard]] constexpr bool getDataPosition(uint32_t& offset, uint32_t& length, ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice = 0, uint16_t sliceCount = 1) {
offset = 0;
length = 0;
@@ -745,9 +759,8 @@ namespace ImageFormatDetails {
if (i == mip && j == frame && k == face && l == slice) {
length = imageSize;
return true;
- } else {
- offset += imageSize;
}
+ offset += imageSize;
}
}
}
@@ -755,6 +768,30 @@ namespace ImageFormatDetails {
return false;
}
+// XTF (PLATFORM_XBOX) is more like DDS layout
+[[nodiscard]] constexpr bool getDataPositionXbox(uint32_t& offset, uint32_t& length, bool padded, ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice = 0, uint16_t sliceCount = 1) {
+ offset = 0;
+ length = 0;
+ for (int j = 0; j < frameCount; j++) {
+ for (int k = 0; k < faceCount; k++) {
+ for (int i = 0; i < mipCount; i++) {
+ for (int l = 0; l < sliceCount; l++) {
+ const auto imageSize = ImageFormatDetails::getDataLength(format, ImageDimensions::getMipDim(i, width), ImageDimensions::getMipDim(i, height));
+ if (i == mip && j == frame && k == face && l == slice) {
+ length = imageSize;
+ return true;
+ }
+ offset += imageSize;
+ }
+ }
+ }
+ if (padded && j + 1 != frameCount && offset > 512) {
+ offset += sourcepp::math::paddingForAlignment(512, offset);
+ }
+ }
+ return false;
+}
+
} // namespace ImageFormatDetails
} // namespace vtfpp
diff --git a/include/vtfpp/ImageQuantize.h b/include/vtfpp/ImageQuantize.h
new file mode 100644
index 000000000..cb2d53d3f
--- /dev/null
+++ b/include/vtfpp/ImageQuantize.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "ImageConversion.h"
+
+namespace vtfpp::ImageQuantize {
+
+/// Converts a paletted image to something usable.
+[[nodiscard]] std::vector convertP8ImageDataToBGRA8888(std::span paletteData, std::span imageData);
+
+} // namespace vtfpp::ImageQuantize
diff --git a/include/vtfpp/VTF.h b/include/vtfpp/VTF.h
index de8ed2c32..0ff3ff413 100644
--- a/include/vtfpp/VTF.h
+++ b/include/vtfpp/VTF.h
@@ -19,6 +19,7 @@
namespace vtfpp {
constexpr uint32_t VTF_SIGNATURE = sourcepp::parser::binary::makeFourCC("VTF\0");
+constexpr uint32_t XTF_SIGNATURE = sourcepp::parser::binary::makeFourCC("XTF\0");
constexpr uint32_t VTFX_SIGNATURE = sourcepp::parser::binary::makeFourCC("VTFX");
constexpr uint32_t VTF3_SIGNATURE = sourcepp::parser::binary::makeFourCC("VTF3");
@@ -33,8 +34,10 @@ enum class CompressionMethod : int16_t {
struct Resource {
enum Type : uint32_t {
- TYPE_UNKNOWN = 0, // Unknown
+ TYPE_UNKNOWN = 0, // Unknown
TYPE_THUMBNAIL_DATA = 1,
+ TYPE_PALETTE_DATA = 2, // Hack for XBOX platform
+ TYPE_FALLBACK_DATA = 3, // Hack for XBOX platform
TYPE_PARTICLE_SHEET_DATA = 16,
TYPE_HOTSPOT_DATA = 43,
TYPE_IMAGE_DATA = 48,
@@ -44,13 +47,16 @@ struct Resource {
TYPE_LOD_CONTROL_INFO = sourcepp::parser::binary::makeFourCC("LOD\0"),
TYPE_KEYVALUES_DATA = sourcepp::parser::binary::makeFourCC("KVD\0"),
};
- static consteval std::array getOrder() {
+ static consteval std::array getOrder() {
return {
TYPE_THUMBNAIL_DATA,
+ TYPE_PALETTE_DATA,
+ TYPE_FALLBACK_DATA,
TYPE_PARTICLE_SHEET_DATA,
TYPE_HOTSPOT_DATA,
// regular Source can't handle out of order resources, but Strata can,
- // and it's the only branch that can read this and 7.6
+ // and it's the only branch that can read this and 7.6.
+ // Put this before the image data to fix resources being cut off when skipping mips
TYPE_AUX_COMPRESSION,
TYPE_IMAGE_DATA,
TYPE_EXTENDED_FLAGS,
@@ -167,6 +173,12 @@ class VTF {
};
static constexpr uint32_t FLAGS_MASK_V2 = FLAG_V2_NO_DEPTH_BUFFER | FLAG_V2_CLAMP_U;
+ enum FlagsXBOX : uint32_t {
+ FLAG_XBOX_CACHEABLE = 1 << 27,
+ FLAG_XBOX_UNFILTERABLE_OK = 1 << 28,
+ };
+ static constexpr uint32_t FLAGS_MASK_XBOX = FLAG_XBOX_CACHEABLE | FLAG_XBOX_UNFILTERABLE_OK;
+
enum FlagsV3 : uint32_t {
FLAG_V3_LOAD_ALL_MIPS = 1 << 10,
FLAG_V3_VERTEX_TEXTURE = 1 << 26,
@@ -211,6 +223,7 @@ class VTF {
enum Platform : uint32_t {
PLATFORM_UNKNOWN = 0x000,
PLATFORM_PC = 0x007,
+ PLATFORM_XBOX = 0x005,
PLATFORM_X360 = 0x360,
PLATFORM_PS3_ORANGEBOX = 0x333,
PLATFORM_PS3_PORTAL2 = 0x334,
@@ -238,6 +251,7 @@ class VTF {
float bumpMapScale = 1.f;
float gammaCorrection = 1.f;
bool invertGreenChannel = false;
+ uint8_t xboxMipScale = 0;
};
/// This value is only valid when passed to VTF::create through CreationOptions
@@ -362,6 +376,12 @@ class VTF {
[[nodiscard]] uint8_t getThumbnailHeight() const;
+ [[nodiscard]] uint8_t getFallbackWidth() const;
+
+ [[nodiscard]] uint8_t getFallbackHeight() const;
+
+ [[nodiscard]] uint8_t getFallbackMipCount() const;
+
[[nodiscard]] const std::vector& getResources() const;
[[nodiscard]] const Resource* getResource(Resource::Type type) const;
@@ -433,6 +453,8 @@ class VTF {
void setThumbnail(std::span imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, float quality = ImageConversion::DEFAULT_COMPRESSED_QUALITY);
+ bool setThumbnail(const std::string& imagePath, float quality = ImageConversion::DEFAULT_COMPRESSED_QUALITY); // NOLINT(*-use-nodiscard)
+
void computeThumbnail(ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::DEFAULT, float quality = ImageConversion::DEFAULT_COMPRESSED_QUALITY);
void removeThumbnail();
@@ -441,6 +463,26 @@ class VTF {
bool saveThumbnailToFile(const std::string& imagePath, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) const; // NOLINT(*-use-nodiscard)
+ [[nodiscard]] bool hasFallbackData() const;
+
+ [[nodiscard]] std::span getFallbackDataRaw(uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0) const;
+
+ [[nodiscard]] std::vector getFallbackDataAs(ImageFormat newFormat, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0) const;
+
+ [[nodiscard]] std::vector getFallbackDataAsRGBA8888(uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0) const;
+
+ void computeFallback(ImageConversion::ResizeFilter filter = ImageConversion::ResizeFilter::DEFAULT);
+
+ void removeFallback();
+
+ [[nodiscard]] std::vector saveFallbackToFile(uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) const;
+
+ bool saveFallbackToFile(const std::string& imagePath, uint8_t mip = 0, uint16_t frame = 0, uint8_t face = 0, ImageConversion::FileFormat fileFormat = ImageConversion::FileFormat::DEFAULT) const; // NOLINT(*-use-nodiscard)
+
+ [[nodiscard]] uint8_t getXBOXMipScale() const;
+
+ void setXBOXMipScale(uint8_t xboxMipScale_);
+
[[nodiscard]] std::vector bake() const;
bool bake(const std::string& vtfPath) const; // NOLINT(*-use-nodiscard)
@@ -460,37 +502,37 @@ class VTF {
std::vector data;
- //uint32_t signature;
- uint32_t version{};
- //uint32_t headerSize;
+ uint32_t version = 4;
- uint16_t width{};
- uint16_t height{};
- uint32_t flags{};
+ uint16_t width = 0;
+ uint16_t height = 0;
+ uint32_t flags = VTF::FLAG_NO_MIP | VTF::FLAG_NO_LOD;
uint16_t frameCount = 1;
- uint16_t startFrame{};
+ uint16_t startFrame = 0;
- //uint8_t _padding0[4];
- sourcepp::math::Vec3f reflectivity{};
- //uint8_t _padding1[4];
+ sourcepp::math::Vec3f reflectivity{0.2f, 0.2f, 0.2f};
- float bumpMapScale{};
+ float bumpMapScale = 0.f;
ImageFormat format = ImageFormat::EMPTY;
uint8_t mipCount = 1;
ImageFormat thumbnailFormat = ImageFormat::EMPTY;
- uint8_t thumbnailWidth{};
- uint8_t thumbnailHeight{};
+ uint8_t thumbnailWidth = 0;
+ uint8_t thumbnailHeight = 0;
+
+ uint8_t fallbackWidth = 0;
+ uint8_t fallbackHeight = 0;
+ uint8_t fallbackMipCount = 0;
+
+ // Number of times to multiply the scale of each mip by 2 when rendering on XBOX
+ uint8_t xboxMipScale = 0;
// 1 for v7.1 and lower
uint16_t sliceCount = 1;
- //uint8_t _padding2[3];
// Technically added in v7.3, but we use it to store image and thumbnail data in v7.2 and lower anyway
- //uint32_t resourceCount;
std::vector resources;
- //uint8_t _padding3[4];
// These aren't in the header
Platform platform = PLATFORM_PC;
diff --git a/src/vtfpp/ImageQuantize.cpp b/src/vtfpp/ImageQuantize.cpp
new file mode 100644
index 000000000..ebe2fdf83
--- /dev/null
+++ b/src/vtfpp/ImageQuantize.cpp
@@ -0,0 +1,19 @@
+#include
+
+using namespace vtfpp;
+
+std::vector ImageQuantize::convertP8ImageDataToBGRA8888(std::span paletteData, std::span imageData) {
+ if (paletteData.size() != 256 * sizeof(ImagePixel::BGRA8888)) {
+ return {};
+ }
+
+ std::span palettePixelData{reinterpret_cast(paletteData.data()), 256};
+
+ std::vector out;
+ out.resize(imageData.size() * sizeof(ImagePixel::BGRA8888));
+ BufferStream stream{out};
+ for (const auto index : imageData) {
+ stream << palettePixelData[static_cast(index)];
+ }
+ return out;
+}
diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp
index bc6a6925c..bac124955 100644
--- a/src/vtfpp/VTF.cpp
+++ b/src/vtfpp/VTF.cpp
@@ -20,6 +20,7 @@
#include
#include
+#include
using namespace sourcepp;
using namespace vtfpp;
@@ -71,9 +72,9 @@ namespace {
return {};
}
-template
-constexpr void swizzleImageDataForPS3(std::span inputData, std::span outputData, ImageFormat format, uint16_t width, uint16_t height, uint16_t slice) {
- width *= ImageFormatDetails::bpp(format) / 32;
+template
+constexpr void swizzleUncompressedImageData(std::span inputData, std::span outputData, ImageFormat format, uint16_t width, uint16_t height, uint16_t slice) {
+ width *= ImageFormatDetails::bpp(format) / (sizeof(T) * 8);
const auto zIndex = [
widthL2 = static_cast(math::log2ceil(width)),
heightL2 = static_cast(math::log2ceil(height)),
@@ -101,15 +102,15 @@ constexpr void swizzleImageDataForPS3(std::span inputData, std::span<
return offset;
};
- const auto* inputPtr = reinterpret_cast(inputData.data());
- auto* outputPtr = reinterpret_cast(outputData.data());
+ const auto* inputPtr = reinterpret_cast(inputData.data());
+ auto* outputPtr = reinterpret_cast(outputData.data());
for (uint16_t x = 0; x < width; x++) {
for (uint16_t y = 0; y < height; y++) {
for (uint16_t z = 0; z < slice; z++) {
if constexpr (ExistingDataIsSwizzled) {
- *outputPtr++ = reinterpret_cast(inputData.data())[zIndex(x, y, z)];
+ *outputPtr++ = reinterpret_cast(inputData.data())[zIndex(x, y, z)];
} else {
- reinterpret_cast(outputData.data())[zIndex(x, y, z)] = *inputPtr++;
+ reinterpret_cast(outputData.data())[zIndex(x, y, z)] = *inputPtr++;
}
}
}
@@ -153,7 +154,8 @@ void swapImageDataEndianForConsole(std::span imageData, ImageFormat f
}
}
- if ((platform == VTF::PLATFORM_PS3_ORANGEBOX || platform == VTF::PLATFORM_PS3_PORTAL2) && !ImageFormatDetails::compressed(format) && ImageFormatDetails::bpp(format) % 32 == 0) {
+ // todo(vtfpp): should we enable 16-bit wide and 8-bit wide formats outside XBOX?
+ if ((platform == VTF::PLATFORM_XBOX || platform == VTF::PLATFORM_PS3_ORANGEBOX || platform == VTF::PLATFORM_PS3_PORTAL2) && !ImageFormatDetails::compressed(format) && (ImageFormatDetails::bpp(format) % 32 == 0 || (platform == VTF::PLATFORM_XBOX && (ImageFormatDetails::bpp(format) % 16 == 0 || ImageFormatDetails::bpp(format) % 8 == 0)))) {
std::vector out(imageData.size());
for(int mip = mipCount - 1; mip >= 0; mip--) {
const auto mipWidth = ImageDimensions::getMipDim(mip, width);
@@ -163,7 +165,13 @@ void swapImageDataEndianForConsole(std::span imageData, ImageFormat f
if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, format, mip, mipCount, frame, frameCount, face, faceCount, width, height)) {
std::span imageDataSpan{imageData.data() + offset, length * sliceCount};
std::span outSpan{out.data() + offset, length * sliceCount};
- ::swizzleImageDataForPS3(imageDataSpan, outSpan, format, mipWidth, mipHeight, sliceCount);
+ if (ImageFormatDetails::bpp(format) % 32 == 0) {
+ ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, sliceCount);
+ } else if (ImageFormatDetails::bpp(format) % 16 == 0) {
+ ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, sliceCount);
+ } else /*if (ImageFormatDetails::bpp(format) % 8 == 0)*/ {
+ ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, sliceCount);
+ }
}
}
}
@@ -172,6 +180,51 @@ void swapImageDataEndianForConsole(std::span imageData, ImageFormat f
}
}
+template
+[[nodiscard]] std::vector convertBetweenDDSAndVTFMipOrderForXBOX(bool padded, std::span imageData, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount, bool& ok) {
+ std::vector reorderedImageData;
+ reorderedImageData.resize(ImageFormatDetails::getDataLengthXBOX(padded, format, mipCount, frameCount, faceCount, width, height, sliceCount));
+ BufferStream reorderedStream{reorderedImageData};
+
+ if constexpr (ConvertingFromDDS) {
+ for (int i = mipCount - 1; i >= 0; i--) {
+ for (int j = 0; j < frameCount; j++) {
+ for (int k = 0; k < faceCount; k++) {
+ for (int l = 0; l < sliceCount; l++) {
+ uint32_t oldOffset, length;
+ if (!ImageFormatDetails::getDataPositionXbox(oldOffset, length, padded, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, sliceCount)) {
+ ok = false;
+ return {};
+ }
+ reorderedStream << imageData.subspan(oldOffset, length);
+ }
+ }
+ }
+ }
+ } else {
+ for (int j = 0; j < frameCount; j++) {
+ for (int k = 0; k < faceCount; k++) {
+ for (int i = 0; i < mipCount; i++) {
+ for (int l = 0; l < sliceCount; l++) {
+ uint32_t oldOffset, length;
+ if (!ImageFormatDetails::getDataPosition(oldOffset, length, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, sliceCount)) {
+ ok = false;
+ return {};
+ }
+ reorderedStream << imageData.subspan(oldOffset, length);
+ }
+ }
+ }
+ if (padded && j + 1 != frameCount && reorderedStream.tell() > 512) {
+ reorderedStream.pad(math::paddingForAlignment(512, reorderedStream.tell()));
+ }
+ }
+ }
+
+ ok = true;
+ return reorderedImageData;
+}
+
} // namespace
Resource::ConvertedData Resource::convertData() const {
@@ -218,19 +271,6 @@ Resource::ConvertedData Resource::convertData() const {
}
VTF::VTF() {
- this->version = 4;
-
- this->flags = VTF::FLAG_NO_MIP | VTF::FLAG_NO_LOD;
-
- this->format = ImageFormat::EMPTY;
- this->thumbnailFormat = ImageFormat::EMPTY;
-
- this->width = 0;
- this->height = 0;
- this->mipCount = 0;
- this->frameCount = 0;
- this->sliceCount = 0;
-
this->opened = true;
}
@@ -264,6 +304,17 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly)
} else {
this->version = 4;
}
+ } else if (signature == XTF_SIGNATURE) {
+ stream >> this->platform;
+ if (this->platform != PLATFORM_XBOX) {
+ return;
+ }
+ stream >> this->version;
+ if (this->version != 0) {
+ return;
+ }
+ // Now fix up the actual version as it would be on PC
+ this->version = 2;
} else {
return;
}
@@ -322,8 +373,8 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly)
}
}
if (lastResource) {
- auto offset = *reinterpret_cast(lastResource->data.data());
- auto curPos = stream.tell();
+ const auto offset = *reinterpret_cast(lastResource->data.data());
+ const auto curPos = stream.tell();
stream.seek(offset);
lastResource->data = stream.read_span(stream.size() - offset);
stream.seek(static_cast(curPos));
@@ -464,7 +515,108 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly)
this->compressionMethod = resource->getDataAsAuxCompressionMethod();
this->removeResourceInternal(Resource::TYPE_AUX_COMPRESSION);
}
- break;
+ return;
+ }
+ case PLATFORM_XBOX: {
+ if (this->platform == PLATFORM_XBOX) {
+ uint16_t preloadSize = 0, imageOffset = 0;
+ stream
+ .read(this->flags)
+ .read(this->width)
+ .read(this->height)
+ .read(this->sliceCount)
+ .read(this->frameCount)
+ .read(preloadSize)
+ .read(imageOffset)
+ .read(this->reflectivity[0])
+ .read(this->reflectivity[1])
+ .read(this->reflectivity[2])
+ .read(this->bumpMapScale)
+ .read(this->format)
+ .read(this->thumbnailWidth)
+ .read(this->thumbnailHeight)
+ .read(this->fallbackWidth)
+ .read(this->fallbackHeight)
+ .read(this->xboxMipScale)
+ .skip(); // padding
+
+ const bool headerSizeIsAccurate = stream.tell() == headerSize;
+
+ this->mipCount = (this->flags & FLAG_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height);
+ this->fallbackMipCount = (this->flags & FLAG_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->fallbackWidth, this->fallbackHeight);
+
+ postReadTransform();
+
+ // Can't use VTF::getFaceCount yet because there's no image data
+ const auto faceCount = (this->flags & FLAG_ENVMAP) ? 6 : 1;
+
+ if (this->thumbnailWidth == 0 || this->thumbnailHeight == 0) {
+ this->thumbnailFormat = ImageFormat::EMPTY;
+ } else {
+ this->thumbnailFormat = ImageFormat::RGB888;
+ this->resources.push_back({
+ .type = Resource::TYPE_THUMBNAIL_DATA,
+ .flags = Resource::FLAG_NONE,
+ .data = stream.read_span(ImageFormatDetails::getDataLength(this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight)),
+ });
+ }
+
+ if (this->format == ImageFormat::P8) {
+ this->resources.push_back({
+ .type = Resource::TYPE_PALETTE_DATA,
+ .flags = Resource::FLAG_NONE,
+ .data = stream.read_span(256 * sizeof(ImagePixel::BGRA8888)),
+ });
+ }
+
+ bool ok;
+ auto fallbackSize = ImageFormatDetails::getDataLengthXBOX(false, this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight);
+ std::vector reorderedFallbackData;
+ if (this->hasFallbackData()) {
+ if (stream.tell() + fallbackSize != preloadSize) {
+ // A couple XTFs that shipped with HL2 are missing the NO_MIP flag. We can detect them by checking the size of the fallback
+ fallbackSize = ImageFormatDetails::getDataLengthXBOX(false, this->format, 1, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight);
+ if (stream.tell() + fallbackSize != preloadSize) {
+ this->opened = false;
+ return;
+ }
+ this->fallbackMipCount = 1;
+ this->mipCount = 1;
+ this->flags |= VTF::FLAG_NO_MIP;
+ }
+ reorderedFallbackData = ::convertBetweenDDSAndVTFMipOrderForXBOX(false, stream.read_span(fallbackSize), this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight, 1, ok);
+ if (!ok) {
+ this->opened = false;
+ return;
+ }
+ ::swapImageDataEndianForConsole(reorderedFallbackData, this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight, 1, this->platform);
+ }
+
+ this->opened = headerSizeIsAccurate;
+ if (parseHeaderOnly) {
+ return;
+ }
+
+ const auto imageSize = ImageFormatDetails::getDataLengthXBOX(true, this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->sliceCount);
+ std::vector reorderedImageData;
+ if (this->hasImageData()) {
+ reorderedImageData = ::convertBetweenDDSAndVTFMipOrderForXBOX(true, stream.seek(imageOffset).read_span(imageSize), this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->sliceCount, ok);
+ if (!ok) {
+ this->opened = false;
+ return;
+ }
+ ::swapImageDataEndianForConsole(reorderedImageData, this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->sliceCount, this->platform);
+ }
+
+ // By this point we cannot use spans over data, it will change here
+ if (this->hasFallbackData()) {
+ this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, reorderedFallbackData);
+ }
+ if (this->hasImageData()) {
+ this->setResourceInternal(Resource::TYPE_IMAGE_DATA, reorderedImageData);
+ }
+ return;
+ }
}
case PLATFORM_X360:
case PLATFORM_PS3_ORANGEBOX:
@@ -484,7 +636,7 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly)
.read(this->reflectivity[2])
.read(this->bumpMapScale)
.read(this->format)
- .skip() // lowResImageSample (replacement for thumbnail resource, presumably linear color)
+ .skip() // lowResImageSample (replacement for thumbnail resource, linear color pixel)
.skip(); // compressedLength
postReadTransform();
@@ -528,7 +680,7 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly)
BufferStream::swap_endian(reinterpret_cast(resource.data.data()));
}
}
- break;
+ return;
}
}
}
@@ -559,6 +711,10 @@ VTF& VTF::operator=(const VTF& other) {
this->thumbnailFormat = other.thumbnailFormat;
this->thumbnailWidth = other.thumbnailWidth;
this->thumbnailHeight = other.thumbnailHeight;
+ this->fallbackWidth = other.fallbackWidth;
+ this->fallbackHeight = other.fallbackHeight;
+ this->fallbackMipCount = other.fallbackMipCount;
+ this->xboxMipScale = other.xboxMipScale;
this->sliceCount = other.sliceCount;
this->resources.clear();
@@ -633,6 +789,7 @@ bool VTF::createInternal(VTF& writer, CreationOptions options) {
}
writer.setCompressionLevel(options.compressionLevel);
writer.setCompressionMethod(options.compressionMethod);
+ writer.setXBOXMipScale(options.xboxMipScale);
return out;
}
@@ -712,6 +869,9 @@ void VTF::setPlatform(Platform newPlatform) {
case PLATFORM_UNKNOWN:
case PLATFORM_PC:
break;
+ case PLATFORM_XBOX:
+ this->setVersion(2);
+ break;
case PLATFORM_X360:
case PLATFORM_PS3_ORANGEBOX:
this->setVersion(4);
@@ -721,6 +881,38 @@ void VTF::setPlatform(Platform newPlatform) {
break;
}
this->platform = newPlatform;
+
+ // Update flags
+ if (this->platform == PLATFORM_XBOX || newPlatform == PLATFORM_XBOX) {
+ this->removeFlags(VTF::FLAGS_MASK_XBOX);
+ }
+
+ // XBOX stores thumbnail as single RGB888 pixel, but we assume thumbnail is DXT1 on other platforms
+ if (this->hasThumbnailData()) {
+ if (this->platform == PLATFORM_XBOX) {
+ this->thumbnailFormat = ImageFormat::RGB888;
+ this->thumbnailWidth = 1;
+ this->thumbnailHeight = 1;
+ std::array newThumbnail{
+ static_cast(static_cast(std::clamp(this->reflectivity[0], 0.f, 1.f) * 255.f)),
+ static_cast(static_cast(std::clamp(this->reflectivity[1], 0.f, 1.f) * 255.f)),
+ static_cast(static_cast(std::clamp(this->reflectivity[2], 0.f, 1.f) * 255.f)),
+ };
+ this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, newThumbnail);
+ } else if (oldPlatform == PLATFORM_XBOX) {
+ this->thumbnailFormat = ImageFormat::EMPTY;
+ this->thumbnailWidth = 0;
+ this->thumbnailHeight = 0;
+ }
+ }
+
+ // Add/remove fallback data for XBOX
+ if (this->platform != PLATFORM_XBOX && this->hasFallbackData()) {
+ this->removeFallback();
+ } else if (this->platform == PLATFORM_XBOX) {
+ this->computeFallback();
+ }
+
this->setCompressionMethod(this->compressionMethod);
if (this->platform != PLATFORM_PC) {
@@ -753,7 +945,7 @@ void VTF::setVersion(uint32_t newVersion) {
}
// Fix up flags
- bool srgb = this->isSRGB();
+ const bool srgb = this->isSRGB();
if ((this->version < 2 && newVersion >= 2) || (this->version >= 2 && newVersion < 2)) {
this->removeFlags(VTF::FLAGS_MASK_V2);
}
@@ -916,6 +1108,7 @@ void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter,
this->format = newFormat;
return;
}
+ const auto oldFormat = this->format;
auto newMipCount = this->mipCount;
if (const auto recommendedCount = ImageDimensions::getRecommendedMipCountForDims(newFormat, this->width, this->height); newMipCount > recommendedCount) {
newMipCount = recommendedCount;
@@ -925,6 +1118,11 @@ void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter,
} else {
this->regenerateImageData(newFormat, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->sliceCount, filter, quality);
}
+
+ if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA)) {
+ const auto fallbackConverted = ImageConversion::convertSeveralImageDataToFormat(fallbackResource->data, oldFormat, this->format, ImageDimensions::getActualMipCountForDimsOnConsole(this->fallbackWidth, this->fallbackHeight), this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, quality);
+ this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, fallbackConverted);
+ }
}
uint8_t VTF::getMipCount() const {
@@ -1170,6 +1368,18 @@ uint8_t VTF::getThumbnailHeight() const {
return this->thumbnailHeight;
}
+uint8_t VTF::getFallbackWidth() const {
+ return this->fallbackWidth;
+}
+
+uint8_t VTF::getFallbackHeight() const {
+ return this->fallbackHeight;
+}
+
+uint8_t VTF::getFallbackMipCount() const {
+ return this->fallbackMipCount;
+}
+
const std::vector& VTF::getResources() const {
return this->resources;
}
@@ -1315,6 +1525,11 @@ void VTF::regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t
this->sliceCount = newSliceCount;
this->setResourceInternal(Resource::TYPE_IMAGE_DATA, newImageData);
+
+ if (this->hasFallbackData()) {
+ this->removeFallback();
+ this->computeFallback();
+ }
}
std::vector VTF::getParticleSheetFrameDataRaw(uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
@@ -1480,6 +1695,11 @@ std::vector VTF::getImageDataAs(ImageFormat newFormat, uint8_t mip, u
if (rawImageData.empty()) {
return {};
}
+ if (this->format == ImageFormat::P8) {
+ if (const auto* palette = this->getResource(Resource::TYPE_PALETTE_DATA)) {
+ return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(palette->data, rawImageData), ImageFormat::BGRA8888, newFormat, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height));
+ }
+ }
return ImageConversion::convertImageDataToFormat(rawImageData, this->format, newFormat, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height));
}
@@ -1589,6 +1809,11 @@ std::vector VTF::getThumbnailDataAs(ImageFormat newFormat) const {
if (rawThumbnailData.empty()) {
return {};
}
+ if (this->thumbnailFormat == ImageFormat::P8) {
+ if (const auto* palette = this->getResource(Resource::TYPE_PALETTE_DATA)) {
+ return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(palette->data, rawThumbnailData), ImageFormat::BGRA8888, newFormat, this->fallbackWidth, this->fallbackHeight);
+ }
+ }
return ImageConversion::convertImageDataToFormat(rawThumbnailData, this->thumbnailFormat, newFormat, this->thumbnailWidth, this->thumbnailHeight);
}
@@ -1606,6 +1831,28 @@ void VTF::setThumbnail(std::span imageData_, ImageFormat format
this->thumbnailHeight = height_;
}
+bool VTF::setThumbnail(const std::string& imagePath, float quality) {
+ ImageFormat inputFormat;
+ int inputWidth, inputHeight, inputFrameCount;
+ auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount);
+
+ // Unable to decode file
+ if (imageData_.empty() || inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) {
+ return false;
+ }
+
+ // One frame (normal)
+ if (inputFrameCount == 1) {
+ this->setThumbnail(imageData_, inputFormat, inputWidth, inputHeight, quality);
+ return true;
+ }
+
+ // Multiple frames (GIF) - we will just use the first one
+ const auto frameSize = ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight);
+ this->setThumbnail({imageData_.data(), frameSize}, inputFormat, inputWidth, inputHeight, quality);
+ return true;
+}
+
void VTF::computeThumbnail(ImageConversion::ResizeFilter filter, float quality) {
if (!this->hasImageData()) {
return;
@@ -1634,6 +1881,93 @@ bool VTF::saveThumbnailToFile(const std::string& imagePath, ImageConversion::Fil
return false;
}
+bool VTF::hasFallbackData() const {
+ return this->fallbackWidth > 0 && this->fallbackHeight > 0 && this->fallbackMipCount > 0;
+}
+
+std::span VTF::getFallbackDataRaw(uint8_t mip, uint16_t frame, uint8_t face) const {
+ if (const auto fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA)) {
+ if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->fallbackMipCount, frame, this->frameCount, face, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight)) {
+ return fallbackResource->data.subspan(offset, length);
+ }
+ }
+ return {};
+}
+
+std::vector VTF::getFallbackDataAs(ImageFormat newFormat, uint8_t mip, uint16_t frame, uint8_t face) const {
+ const auto rawFallbackData = this->getFallbackDataRaw(mip, frame, face);
+ if (rawFallbackData.empty()) {
+ return {};
+ }
+ if (this->format == ImageFormat::P8) {
+ if (const auto* palette = this->getResource(Resource::TYPE_PALETTE_DATA)) {
+ return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(palette->data, rawFallbackData), ImageFormat::BGRA8888, newFormat, this->fallbackWidth, this->fallbackHeight);
+ }
+ }
+ return ImageConversion::convertImageDataToFormat(rawFallbackData, this->format, newFormat, this->fallbackWidth, this->fallbackHeight);
+}
+
+std::vector VTF::getFallbackDataAsRGBA8888(uint8_t mip, uint16_t frame, uint8_t face) const {
+ return this->getFallbackDataAs(ImageFormat::RGBA8888, mip, frame, face);
+}
+
+void VTF::computeFallback(ImageConversion::ResizeFilter filter) {
+ if (!this->hasImageData()) {
+ return;
+ }
+
+ const auto* imageResource = this->getResourceInternal(Resource::TYPE_IMAGE_DATA);
+ if (!imageResource) {
+ return;
+ }
+
+ const auto faceCount = this->getFaceCount();
+
+ this->fallbackWidth = 8;
+ this->fallbackHeight = 8;
+ this->fallbackMipCount = ImageDimensions::getActualMipCountForDimsOnConsole(this->fallbackWidth, this->fallbackHeight);
+
+ std::vector fallbackData;
+ fallbackData.resize(ImageFormatDetails::getDataLength(this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight));
+ for (int i = this->fallbackMipCount - 1; i >= 0; i--) {
+ for (int j = 0; j < this->frameCount; j++) {
+ for (int k = 0; k < faceCount; k++) {
+ auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(0, j, k, 0), this->format, this->width, ImageDimensions::getMipDim(i, this->fallbackWidth), this->height, ImageDimensions::getMipDim(i, this->fallbackHeight), this->isSRGB(), filter);
+ if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->fallbackMipCount, j, this->frameCount, k, faceCount, this->fallbackWidth, this->fallbackHeight) && mip.size() == length) {
+ std::memcpy(fallbackData.data() + offset, mip.data(), length);
+ }
+ }
+ }
+ }
+ this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, fallbackData);
+}
+
+void VTF::removeFallback() {
+ this->fallbackWidth = 0;
+ this->fallbackHeight = 0;
+ this->fallbackMipCount = 0;
+ this->removeResourceInternal(Resource::TYPE_FALLBACK_DATA);
+}
+
+std::vector VTF::saveFallbackToFile(uint8_t mip, uint16_t frame, uint8_t face, ImageConversion::FileFormat fileFormat) const {
+ return ImageConversion::convertImageDataToFile(this->getFallbackDataRaw(mip, frame, face), this->format, ImageDimensions::getMipDim(mip, this->fallbackWidth), ImageDimensions::getMipDim(mip, this->fallbackHeight), fileFormat);
+}
+
+bool VTF::saveFallbackToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, ImageConversion::FileFormat fileFormat) const {
+ if (auto data_ = this->saveFallbackToFile(mip, frame, face, fileFormat); !data_.empty()) {
+ return fs::writeFileBuffer(imagePath, data_);
+ }
+ return false;
+}
+
+uint8_t VTF::getXBOXMipScale() const {
+ return this->xboxMipScale;
+}
+
+void VTF::setXBOXMipScale(uint8_t xboxMipScale_) {
+ this->xboxMipScale = xboxMipScale_;
+}
+
std::vector VTF::bake() const {
std::vector out;
BufferStream writer{out};
@@ -1664,7 +1998,7 @@ std::vector VTF::bake() const {
switch (this->platform) {
case PLATFORM_UNKNOWN:
- break;
+ return out;
case PLATFORM_PC: {
writer
.write(VTF_SIGNATURE)
@@ -1680,9 +2014,9 @@ std::vector VTF::bake() const {
.write(bakeFlags)
.write(this->frameCount)
.write(this->startFrame)
- .write(0) // padding
+ .pad()
.write(this->reflectivity)
- .write(0) // padding
+ .pad()
.write(this->bumpMapScale)
.write(bakeFormat)
.write(this->mipCount)
@@ -1695,10 +2029,7 @@ std::vector VTF::bake() const {
}
if (this->version < 3) {
- const auto headerAlignment = math::paddingForAlignment(16, writer.tell());
- for (uint16_t i = 0; i < headerAlignment; i++) {
- writer.write({});
- }
+ writer.pad(math::paddingForAlignment(16, writer.tell()));
const auto headerSize = writer.tell();
writer.seek_u(headerLengthPos).write(headerSize).seek_u(headerSize);
@@ -1742,12 +2073,7 @@ std::vector VTF::bake() const {
}
}
- writer
- .write(0) // padding
- .write(0) // padding
- .write(0) // padding
- .write(this->getResources().size() + hasAuxCompression)
- .write(0); // padding
+ writer.pad(3).write(this->getResources().size() + hasAuxCompression).pad(8);
const auto resourceStart = writer.tell();
const auto headerSize = resourceStart + ((this->getResources().size() + hasAuxCompression) * sizeof(uint64_t));
@@ -1772,9 +2098,86 @@ std::vector VTF::bake() const {
}
}
}
+ break;
+ }
+ case PLATFORM_XBOX: {
+ writer << XTF_SIGNATURE << PLATFORM_XBOX;
+ writer.write(0);
- out.resize(writer.size());
- return out;
+ const auto headerSizePos = writer.tell();
+ writer
+ .write(0)
+ .write(this->flags)
+ .write(this->width)
+ .write(this->height)
+ .write(this->sliceCount)
+ .write(this->frameCount);
+ const auto preloadSizePos = writer.tell();
+ writer.write(0);
+ const auto imageOffsetPos = writer.tell();
+ writer
+ .write(0)
+ .write(this->reflectivity[0])
+ .write(this->reflectivity[1])
+ .write(this->reflectivity[2])
+ .write(this->bumpMapScale)
+ .write(this->format)
+ .write(this->thumbnailWidth)
+ .write(this->thumbnailHeight)
+ .write(this->fallbackWidth)
+ .write(this->fallbackHeight)
+ .write(this->xboxMipScale)
+ .write(0);
+
+ const auto headerSize = writer.tell();
+ writer.seek_u(headerSizePos).write(headerSize).seek_u(headerSize);
+
+ if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
+ writer.write(thumbnailResource->data);
+ }
+
+ if (this->format == ImageFormat::P8) {
+ if (const auto* paletteResource = this->getResource(Resource::TYPE_PALETTE_DATA)) {
+ writer.write(paletteResource->data);
+ }
+ }
+
+ bool hasFallbackResource = false;
+ if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA); fallbackResource && this->hasFallbackData()) {
+ hasFallbackResource = true;
+ bool ok;
+ auto reorderedFallbackData = ::convertBetweenDDSAndVTFMipOrderForXBOX(false, fallbackResource->data, this->format, this->fallbackMipCount, this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, ok);
+ if (ok) {
+ ::swapImageDataEndianForConsole(reorderedFallbackData, this->format, this->fallbackMipCount, this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, this->platform);
+ writer.write(reorderedFallbackData);
+ } else {
+ writer.pad(fallbackResource->data.size());
+ }
+ }
+
+ const auto preloadSize = writer.tell();
+ writer.seek_u(preloadSizePos).write(preloadSize).seek_u(preloadSize);
+
+ if (hasFallbackResource) {
+ writer.pad(math::paddingForAlignment(512, writer.tell()));
+ }
+ const auto imageOffset = writer.tell();
+ writer.seek_u(imageOffsetPos).write(imageOffset).seek_u(imageOffset);
+
+ if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
+ bool ok;
+ auto reorderedImageData = ::convertBetweenDDSAndVTFMipOrderForXBOX(true, imageResource->data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->sliceCount, ok);
+ if (ok) {
+ ::swapImageDataEndianForConsole(reorderedImageData, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->sliceCount, this->platform);
+ writer.write(reorderedImageData);
+ } else {
+ writer.pad(imageResource->data.size());
+ }
+ }
+ if (writer.tell() > 512) {
+ writer.pad(math::paddingForAlignment(512, writer.tell()));
+ }
+ break;
}
case PLATFORM_X360:
case PLATFORM_PS3_ORANGEBOX:
@@ -1788,6 +2191,7 @@ std::vector VTF::bake() const {
writer.set_big_endian(true);
writer << this->platform;
}
+ writer.write(8);
// Go down until top level texture is <1mb, matches makegamedata.exe output
uint8_t mipSkip = 0;
@@ -1797,7 +2201,6 @@ std::vector VTF::bake() const {
}
}
- writer.write(8);
const auto headerLengthPos = writer.tell();
writer
.write(0)
@@ -1894,12 +2297,11 @@ std::vector VTF::bake() const {
}
}
}
-
- out.resize(writer.size());
- return out;
+ break;
}
}
- return {};
+ out.resize(writer.size());
+ return out;
}
bool VTF::bake(const std::string& vtfPath) const {
diff --git a/src/vtfpp/_vtfpp.cmake b/src/vtfpp/_vtfpp.cmake
index 83efc8d12..f15b6ae26 100644
--- a/src/vtfpp/_vtfpp.cmake
+++ b/src/vtfpp/_vtfpp.cmake
@@ -4,6 +4,7 @@ add_pretty_parser(vtfpp
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/HOT.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageConversion.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageFormats.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/ImageQuantize.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/PPL.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/SHT.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vtfpp/TTX.h"
@@ -12,6 +13,7 @@ add_pretty_parser(vtfpp
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/HOT.cpp"
"${CMAKE_CURRENT_LIST_DIR}/ImageConversion.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/ImageQuantize.cpp"
"${CMAKE_CURRENT_LIST_DIR}/PPL.cpp"
"${CMAKE_CURRENT_LIST_DIR}/SHT.cpp"
"${CMAKE_CURRENT_LIST_DIR}/TTX.cpp"
diff --git a/test/vcryptpp.cpp b/test/vcryptpp.cpp
index 06d6578f1..a5df067ba 100644
--- a/test/vcryptpp.cpp
+++ b/test/vcryptpp.cpp
@@ -35,7 +35,6 @@ TEST(vcryptpp, vfont_encrypt) {
const auto decrypted = fs::readFileBuffer(ASSET_ROOT "vcryptpp/test.ttf");
const auto test = VFONT::decrypt(VFONT::encrypt(decrypted));
- fs::writeFileBuffer(ASSET_ROOT "vcryptpp/test_wringer.ttf", test);
ASSERT_EQ(test.size(), decrypted.size());
for (int i = 0; i < test.size(); i++) {
diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp
index cac1d3bab..b3d3b290f 100644
--- a/test/vtfpp.cpp
+++ b/test/vtfpp.cpp
@@ -917,6 +917,271 @@ TEST(vtfpp, read_v75_nothumb_nomip) {
EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLength(vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
}
+TEST(vtfpp, read_xbox) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/dxt1.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 256);
+ EXPECT_EQ(vtf.getHeight(), 256);
+ EXPECT_EQ(vtf.getFlags(), /*NICE filtered*/ (1 << 24) | VTF::FLAG_XBOX_CACHEABLE);
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1);
+ EXPECT_EQ(vtf.getMipCount(), 9);
+ EXPECT_EQ(vtf.getFrameCount(), 1);
+ EXPECT_EQ(vtf.getFaceCount(), 1);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.27086672f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.26922473f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.24819961f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::RGB888);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 1);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 1);
+ EXPECT_EQ(vtf.getFallbackWidth(), 8);
+ EXPECT_EQ(vtf.getFallbackHeight(), 8);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 3);
+
+ const auto* thumbnail = vtf.getResource(Resource::TYPE_THUMBNAIL_DATA);
+ ASSERT_TRUE(thumbnail);
+ EXPECT_EQ(thumbnail->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(thumbnail->data.size(), ImageFormatDetails::getDataLength(vtf.getThumbnailFormat(), vtf.getThumbnailWidth(), vtf.getThumbnailHeight()));
+
+ const auto* fallback = vtf.getResource(Resource::TYPE_FALLBACK_DATA);
+ ASSERT_TRUE(fallback);
+ EXPECT_EQ(fallback->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(fallback->data.size(), ImageFormatDetails::getDataLengthXBOX(false, vtf.getFormat(), vtf.getFallbackMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getFallbackWidth(), vtf.getFallbackHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
+TEST(vtfpp, read_xbox_animated) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/animated.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 128);
+ EXPECT_EQ(vtf.getHeight(), 128);
+ EXPECT_EQ(vtf.getFlags(), /*NICE filtered*/ (1 << 24) | VTF::FLAG_XBOX_CACHEABLE);
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1);
+ EXPECT_EQ(vtf.getMipCount(), 8);
+ EXPECT_EQ(vtf.getFrameCount(), 104);
+ EXPECT_EQ(vtf.getFaceCount(), 1);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.21408364f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.2140833f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.21408342f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::RGB888);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 1);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 1);
+ EXPECT_EQ(vtf.getFallbackWidth(), 8);
+ EXPECT_EQ(vtf.getFallbackHeight(), 8);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 3);
+
+ const auto* thumbnail = vtf.getResource(Resource::TYPE_THUMBNAIL_DATA);
+ ASSERT_TRUE(thumbnail);
+ EXPECT_EQ(thumbnail->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(thumbnail->data.size(), ImageFormatDetails::getDataLength(vtf.getThumbnailFormat(), vtf.getThumbnailWidth(), vtf.getThumbnailHeight()));
+
+ const auto* fallback = vtf.getResource(Resource::TYPE_FALLBACK_DATA);
+ ASSERT_TRUE(fallback);
+ EXPECT_EQ(fallback->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(fallback->data.size(), ImageFormatDetails::getDataLengthXBOX(false, vtf.getFormat(), vtf.getFallbackMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getFallbackWidth(), vtf.getFallbackHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
+TEST(vtfpp, read_xbox_envmap) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/envmap.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 32);
+ EXPECT_EQ(vtf.getHeight(), 32);
+ EXPECT_EQ(vtf.getFlags(), VTF::FLAG_CLAMP_S | VTF::FLAG_CLAMP_T | VTF::FLAG_NO_MIP | VTF::FLAG_ENVMAP | static_cast(VTF::FLAG_XBOX_CACHEABLE));
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1);
+ EXPECT_EQ(vtf.getMipCount(), 1);
+ EXPECT_EQ(vtf.getFrameCount(), 1);
+ EXPECT_EQ(vtf.getFaceCount(), 6);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.034028269f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.020980936f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.013155934f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::EMPTY);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 0);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 0);
+ EXPECT_EQ(vtf.getFallbackWidth(), 8);
+ EXPECT_EQ(vtf.getFallbackHeight(), 8);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 2);
+
+ const auto* fallback = vtf.getResource(Resource::TYPE_FALLBACK_DATA);
+ ASSERT_TRUE(fallback);
+ EXPECT_EQ(fallback->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(fallback->data.size(), ImageFormatDetails::getDataLengthXBOX(false, vtf.getFormat(), vtf.getFallbackMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getFallbackWidth(), vtf.getFallbackHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
+TEST(vtfpp, read_xbox_missing_no_mip_flag) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/missing_no_mip_flag.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 256);
+ EXPECT_EQ(vtf.getHeight(), 256);
+ EXPECT_EQ(vtf.getFlags(), /*HINT_DXT5*/ (1 << 5) | VTF::FLAG_NO_MIP | /*NICE filtered*/ (1 << 24) | VTF::FLAG_XBOX_CACHEABLE);
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT5);
+ EXPECT_EQ(vtf.getMipCount(), 1);
+ EXPECT_EQ(vtf.getFrameCount(), 1);
+ EXPECT_EQ(vtf.getFaceCount(), 1);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.00047894727f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.0032725455f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.0049570859f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::RGB888);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 1);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 1);
+ EXPECT_EQ(vtf.getFallbackWidth(), 8);
+ EXPECT_EQ(vtf.getFallbackHeight(), 8);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 3);
+
+ const auto* thumbnail = vtf.getResource(Resource::TYPE_THUMBNAIL_DATA);
+ ASSERT_TRUE(thumbnail);
+ EXPECT_EQ(thumbnail->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(thumbnail->data.size(), ImageFormatDetails::getDataLength(vtf.getThumbnailFormat(), vtf.getThumbnailWidth(), vtf.getThumbnailHeight()));
+
+ const auto* fallback = vtf.getResource(Resource::TYPE_FALLBACK_DATA);
+ ASSERT_TRUE(fallback);
+ EXPECT_EQ(fallback->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(fallback->data.size(), ImageFormatDetails::getDataLengthXBOX(false, vtf.getFormat(), vtf.getFallbackMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getFallbackWidth(), vtf.getFallbackHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
+TEST(vtfpp, read_xbox_no_fallback) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/no_fallback.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 16);
+ EXPECT_EQ(vtf.getHeight(), 16);
+ EXPECT_EQ(vtf.getFlags(), VTF::FLAG_NO_MIP | VTF::FLAG_MULTI_BIT_ALPHA | /*NICE filtered*/ (1 << 24) | static_cast(VTF::FLAG_XBOX_CACHEABLE));
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT5);
+ EXPECT_EQ(vtf.getMipCount(), 1);
+ EXPECT_EQ(vtf.getFrameCount(), 1);
+ EXPECT_EQ(vtf.getFaceCount(), 1);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.38589939f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.38589939f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.38589939f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::RGB888);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 1);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 1);
+ EXPECT_EQ(vtf.getFallbackWidth(), 0);
+ EXPECT_EQ(vtf.getFallbackHeight(), 0);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 2);
+
+ const auto* thumbnail = vtf.getResource(Resource::TYPE_THUMBNAIL_DATA);
+ ASSERT_TRUE(thumbnail);
+ EXPECT_EQ(thumbnail->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(thumbnail->data.size(), ImageFormatDetails::getDataLength(vtf.getThumbnailFormat(), vtf.getThumbnailWidth(), vtf.getThumbnailHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
+TEST(vtfpp, read_xbox_p8) {
+ VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/xbox/p8.xtf")};
+ ASSERT_TRUE(vtf);
+
+ // Header
+ EXPECT_EQ(vtf.getPlatform(), VTF::PLATFORM_XBOX);
+ EXPECT_EQ(vtf.getVersion(), 2);
+ EXPECT_EQ(vtf.getWidth(), 256);
+ EXPECT_EQ(vtf.getHeight(), 256);
+ EXPECT_EQ(vtf.getFlags(), /*NOCOMPRESS*/ (1 << 6) | /*NICE filtered*/ (1 << 24) | /*Prefiltered*/ (1 << 26) | static_cast(VTF::FLAG_XBOX_CACHEABLE));
+ EXPECT_EQ(vtf.getFormat(), ImageFormat::P8);
+ EXPECT_EQ(vtf.getMipCount(), 9);
+ EXPECT_EQ(vtf.getFrameCount(), 1);
+ EXPECT_EQ(vtf.getFaceCount(), 1);
+ EXPECT_EQ(vtf.getSliceCount(), 1);
+ EXPECT_EQ(vtf.getStartFrame(), 0);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[0], 0.f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[1], 0.068392977f);
+ EXPECT_FLOAT_EQ(vtf.getReflectivity()[2], 0.077002399f);
+ EXPECT_FLOAT_EQ(vtf.getBumpMapScale(), 1.f);
+ EXPECT_EQ(vtf.getThumbnailFormat(), ImageFormat::RGB888);
+ EXPECT_EQ(vtf.getThumbnailWidth(), 1);
+ EXPECT_EQ(vtf.getThumbnailHeight(), 1);
+ EXPECT_EQ(vtf.getFallbackWidth(), 8);
+ EXPECT_EQ(vtf.getFallbackHeight(), 8);
+
+ // Resources
+ EXPECT_EQ(vtf.getResources().size(), 4);
+
+ const auto* thumbnail = vtf.getResource(Resource::TYPE_THUMBNAIL_DATA);
+ ASSERT_TRUE(thumbnail);
+ EXPECT_EQ(thumbnail->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(thumbnail->data.size(), ImageFormatDetails::getDataLength(vtf.getThumbnailFormat(), vtf.getThumbnailWidth(), vtf.getThumbnailHeight()));
+
+ const auto* palette = vtf.getResource(Resource::TYPE_PALETTE_DATA);
+ ASSERT_TRUE(palette);
+ EXPECT_EQ(palette->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(palette->data.size(), 256 * sizeof(ImagePixel::BGRA8888));
+
+ const auto* fallback = vtf.getResource(Resource::TYPE_FALLBACK_DATA);
+ ASSERT_TRUE(fallback);
+ EXPECT_EQ(fallback->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(fallback->data.size(), ImageFormatDetails::getDataLengthXBOX(false, vtf.getFormat(), vtf.getFallbackMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getFallbackWidth(), vtf.getFallbackHeight()));
+
+ const auto* image = vtf.getResource(Resource::TYPE_IMAGE_DATA);
+ ASSERT_TRUE(image);
+ EXPECT_EQ(image->flags, Resource::FLAG_NONE);
+ EXPECT_EQ(image->data.size(), ImageFormatDetails::getDataLengthXBOX(true, vtf.getFormat(), vtf.getMipCount(), vtf.getFrameCount(), vtf.getFaceCount(), vtf.getWidth(), vtf.getHeight(), vtf.getSliceCount()));
+}
+
TEST(vtfpp, read_ps3_orangebox) {
VTF vtf{fs::readFileBuffer(ASSET_ROOT "vtfpp/ps3_orangebox/portal.ps3.vtf")};
ASSERT_TRUE(vtf);