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);