From 1c9ddbc2bf5c67fd120f131c83e445a82635fda1 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 23 Oct 2025 01:55:01 -0700 Subject: [PATCH 01/17] vtfpp: change maximum mip count on pc to the same algorithm used for console platforms --- include/vtfpp/ImageFormats.h | 55 ++++--------------------- lang/c/include/vtfppc/ImageFormats.h | 4 +- lang/c/src/vtfppc/ImageFormats.cpp | 8 +--- lang/python/src/vtfpp.h | 3 +- src/vtfpp/VTF.cpp | 60 ++++++++++------------------ test/vtfpp.cpp | 12 +++--- 6 files changed, 38 insertions(+), 104 deletions(-) diff --git a/include/vtfpp/ImageFormats.h b/include/vtfpp/ImageFormats.h index b6229785f..de7306368 100644 --- a/include/vtfpp/ImageFormats.h +++ b/include/vtfpp/ImageFormats.h @@ -654,56 +654,15 @@ namespace ImageDimensions { return dim; } -[[nodiscard]] constexpr uint8_t getRecommendedMipCountForDims(ImageFormat format, uint16_t width, uint16_t height) { - // Note that compressed formats need 4x4 pixel blocks - if (sourcepp::math::isPowerOf2(width) && sourcepp::math::isPowerOf2(height)) { - const auto log2 = std::bit_width(width > height ? height : width); - if (!ImageFormatDetails::compressed(format)) { - return log2; - } - // Eliminate 2x2, 1x1 - return log2 - 2 > 1 ? log2 - 2 : 1; - } - - uint8_t maxMipCount = 1; - if (ImageFormatDetails::compressed(format)) { - while (width > 0 && height > 0 && width % 4 == 0 && height % 4 == 0) { - width /= 2; - height /= 2; - maxMipCount++; - } - } else { - while (width > 0 && height > 0 && width % 2 == 0 && height % 2 == 0) { - width /= 2; - height /= 2; - maxMipCount++; - } - } - return maxMipCount; -} - -[[nodiscard]] constexpr uint8_t getActualMipCountForDimsOnConsole(uint16_t width, uint16_t height, uint16_t depth = 1) { - if (width == 0 || height == 0 || depth == 0) { - return 0; - } +[[nodiscard]] constexpr uint8_t getMaximumMipCount(uint16_t width, uint16_t height, uint16_t depth = 1) { uint8_t numMipLevels = 1; - while (true) { - if (width == 1 && height == 1 && depth == 1) { - break; - } - width >>= 1; - if (width < 1) { - width = 1; - } - height >>= 1; - if (height < 1) { - height = 1; - } - depth >>= 1; - if (depth < 1) { - depth = 1; + if (width && height && depth) { + while (width > 1 || height > 1 || depth > 1) { + if ((width >>= 1) < 1) width = 1; + if ((height >>= 1) < 1) height = 1; + if ((depth >>= 1) < 1) depth = 1; + numMipLevels++; } - numMipLevels += 1; } return numMipLevels; } diff --git a/lang/c/include/vtfppc/ImageFormats.h b/lang/c/include/vtfppc/ImageFormats.h index ad1b64582..c980dabad 100644 --- a/lang/c/include/vtfppc/ImageFormats.h +++ b/lang/c/include/vtfppc/ImageFormats.h @@ -104,9 +104,7 @@ SOURCEPP_API int vtfpp_image_format_details_console(vtfpp_image_format_e format) SOURCEPP_API uint32_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, uint16_t dim); -SOURCEPP_API uint8_t vtfpp_image_dimensions_get_recommended_mip_count_for_dim(vtfpp_image_format_e format, uint16_t width, uint16_t height); - -SOURCEPP_API uint8_t vtfpp_image_dimensions_get_actual_mip_count_for_dims_on_console(uint16_t width, uint16_t height, uint16_t depth); +SOURCEPP_API uint8_t vtfpp_image_dimensions_get_maximum_mip_count(uint16_t width, uint16_t height, uint16_t depth); SOURCEPP_API uint32_t vtfpp_image_format_details_get_data_length(vtfpp_image_format_e format, uint16_t width, uint16_t height, uint16_t depth); diff --git a/lang/c/src/vtfppc/ImageFormats.cpp b/lang/c/src/vtfppc/ImageFormats.cpp index 896592580..44250b004 100644 --- a/lang/c/src/vtfppc/ImageFormats.cpp +++ b/lang/c/src/vtfppc/ImageFormats.cpp @@ -74,12 +74,8 @@ SOURCEPP_API uint32_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, uint16_t d return ImageDimensions::getMipDim(mip, dim); } -SOURCEPP_API uint8_t vtfpp_image_dimensions_get_recommended_mip_count_for_dim(vtfpp_image_format_e format, uint16_t width, uint16_t height) { - return ImageDimensions::getRecommendedMipCountForDims(static_cast(format), width, height); -} - -SOURCEPP_API uint8_t vtfpp_image_dimensions_get_actual_mip_count_for_dims_on_console(uint16_t width, uint16_t height, uint16_t depth) { - return ImageDimensions::getActualMipCountForDimsOnConsole(width, height, depth); +SOURCEPP_API uint8_t vtfpp_image_dimensions_get_maximum_mip_count(uint16_t width, uint16_t height, uint16_t depth) { + return ImageDimensions::getMaximumMipCount(width, height, depth); } SOURCEPP_API uint32_t vtfpp_image_format_details_get_data_length(vtfpp_image_format_e format, uint16_t width, uint16_t height, uint16_t depth) { diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h index e3c1992d6..0ad7c9315 100644 --- a/lang/python/src/vtfpp.h +++ b/lang/python/src/vtfpp.h @@ -142,8 +142,7 @@ inline void register_python(py::module_& m) { ImageDimensions .def("get_mip_dim", &getMipDim, "mip"_a, "dim"_a) - .def("get_recommended_mip_count_for_dims", &getRecommendedMipCountForDims, "format"_a, "width"_a, "height"_a) - .def("get_actual_mip_count_for_dims_on_console", &getActualMipCountForDimsOnConsole, "width"_a, "height"_a, "depth"_a = 1); + .def("get_maximum_mip_count", &getMaximumMipCount, "width"_a, "height"_a, "depth"_a = 1); } // Skip ImagePixel, difficult and pointless to bind diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index dc7091857..2b37840cf 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -537,8 +537,8 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) const bool headerSizeIsAccurate = stream.tell() == headerSize; - this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height, this->depth); - this->fallbackMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->fallbackWidth, this->fallbackHeight); + this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth); + this->fallbackMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight); postReadTransform(); @@ -641,7 +641,7 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) stream.skip(); } - this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height, this->depth); + this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth); if (parseHeaderOnly) { this->opened = true; @@ -771,8 +771,8 @@ bool VTF::createInternal(VTF& writer, CreationOptions options) { options.outputFormat = VTF::getDefaultCompressedFormat(writer.getFormat(), writer.getVersion(), options.isCubeMap); } if (options.computeMips) { - if (const auto recommendedMipCount = ImageDimensions::getRecommendedMipCountForDims(options.outputFormat, writer.getWidth(), writer.getHeight()); recommendedMipCount > 1) { - if (!writer.setMipCount(recommendedMipCount)) { + if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(writer.getWidth(), writer.getHeight(), writer.getDepth()); maxMipCount > 1) { + if (!writer.setMipCount(maxMipCount)) { out = false; } writer.computeMips(options.filter); @@ -914,14 +914,9 @@ void VTF::setPlatform(Platform newPlatform) { this->setCompressionMethod(this->compressionMethod); if (this->platform != PLATFORM_PC) { - const auto recommendedCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height, this->depth); - if (this->mipCount != recommendedCount) { - this->setMipCount(recommendedCount); - } - } else if (oldPlatform != PLATFORM_PC) { - const auto recommendedMipCount = ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height); - if (this->mipCount > recommendedMipCount) { - this->setMipCount(recommendedMipCount); + const auto maxMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth); + if (this->mipCount != maxMipCount) { + this->setMipCount(maxMipCount); } } } @@ -1008,12 +1003,8 @@ void VTF::setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::Resize return; } auto newMipCount = this->mipCount; - if (this->platform == PLATFORM_PC) { - if (const auto recommendedCount = ImageDimensions::getRecommendedMipCountForDims(this->format, newWidth, newHeight); newMipCount > recommendedCount) { - newMipCount = recommendedCount; - } - } else { - newMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(newWidth, newHeight, this->depth); + if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(newWidth, newHeight, this->depth); this->platform != PLATFORM_PC || newMipCount > maxMipCount) { + newMipCount = maxMipCount; } this->regenerateImageData(this->format, newWidth, newHeight, newMipCount, this->frameCount, this->getFaceCount(), this->depth, filter); } else { @@ -1108,18 +1099,14 @@ void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter, 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; - } if (ImageFormatDetails::compressed(newFormat)) { - this->regenerateImageData(newFormat, this->width + math::paddingForAlignment(4, this->width), this->height + math::paddingForAlignment(4, this->height), newMipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); + this->regenerateImageData(newFormat, this->width + math::paddingForAlignment(4, this->width), this->height + math::paddingForAlignment(4, this->height), this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); } else { - this->regenerateImageData(newFormat, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); + this->regenerateImageData(newFormat, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), this->depth, 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); + const auto fallbackConverted = ImageConversion::convertSeveralImageDataToFormat(fallbackResource->data, oldFormat, this->format, ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight), this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, quality); this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, fallbackConverted); } } @@ -1132,23 +1119,18 @@ bool VTF::setMipCount(uint8_t newMipCount) { if (!this->hasImageData()) { return false; } - if (this->platform != PLATFORM_PC && newMipCount > 1) { - newMipCount = ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height, this->depth); - } else if (const auto recommended = ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height); newMipCount > recommended) { - newMipCount = recommended; - if (newMipCount == 1) { - return false; - } + if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth); (this->platform != PLATFORM_PC && newMipCount > 1) || (this->platform == PLATFORM_PC && newMipCount > maxMipCount)) { + newMipCount = maxMipCount; } this->regenerateImageData(this->format, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->depth); return true; } bool VTF::setRecommendedMipCount() { - if (this->platform == PLATFORM_PC) { - return this->setMipCount(ImageDimensions::getRecommendedMipCountForDims(this->format, this->width, this->height)); - } - return this->setMipCount(ImageDimensions::getActualMipCountForDimsOnConsole(this->width, this->height, this->depth)); + // Something we could do here for v5 and up is check if FLAG_V5_LOAD_MOST_MIPS is set and change mip count accordingly + // (eliminate anything below 32x32). Noting we can't do that check for v4 and down because TF2 branch added small mip + // loading, didn't bother adding the most mips flag, and doesn't handle VTFs missing lower mips correctly. Thanks Valve! + return this->setMipCount(ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth)); } void VTF::computeMips(ImageConversion::ResizeFilter filter) { @@ -1734,7 +1716,7 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u resizedWidth += math::paddingForAlignment(4, resizedWidth); resizedHeight += math::paddingForAlignment(4, resizedHeight); } - if (const auto newMipCount = ImageDimensions::getRecommendedMipCountForDims(format_, resizedWidth, resizedHeight); newMipCount <= mip) { + if (const auto newMipCount = ImageDimensions::getMaximumMipCount(resizedWidth, resizedHeight, this->depth); newMipCount <= mip) { mip = newMipCount - 1; } if (face > 6 || (face == 6 && (this->version < 1 || this->version > 4))) { @@ -1935,7 +1917,7 @@ void VTF::computeFallback(ImageConversion::ResizeFilter filter) { this->fallbackWidth = 8; this->fallbackHeight = 8; - this->fallbackMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getActualMipCountForDimsOnConsole(this->fallbackWidth, this->fallbackHeight); + this->fallbackMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight); std::vector fallbackData; fallbackData.resize(ImageFormatDetails::getDataLength(this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight)); diff --git a/test/vtfpp.cpp b/test/vtfpp.cpp index 885f849e1..45c727496 100644 --- a/test/vtfpp.cpp +++ b/test/vtfpp.cpp @@ -210,7 +210,7 @@ TEST(vtfpp, write_non_po2) { EXPECT_EQ(vtf.getHeight(), 148); EXPECT_EQ(vtf.getFlags(), 0); EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1); - EXPECT_EQ(vtf.getMipCount(), 2); + EXPECT_EQ(vtf.getMipCount(), 8); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); @@ -274,7 +274,7 @@ TEST(vtfpp, write_v70) { EXPECT_EQ(vtf.getHeight(), 256); EXPECT_EQ(vtf.getFlags(), 0); EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1); - EXPECT_EQ(vtf.getMipCount(), 7); + EXPECT_EQ(vtf.getMipCount(), 9); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); @@ -448,7 +448,7 @@ TEST(vtfpp, write_v71) { EXPECT_EQ(vtf.getHeight(), 256); EXPECT_EQ(vtf.getFlags(), 0); EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1); - EXPECT_EQ(vtf.getMipCount(), 7); + EXPECT_EQ(vtf.getMipCount(), 9); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); @@ -622,7 +622,7 @@ TEST(vtfpp, write_v72) { EXPECT_EQ(vtf.getHeight(), 256); EXPECT_EQ(vtf.getFlags(), 0); EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1); - EXPECT_EQ(vtf.getMipCount(), 7); + EXPECT_EQ(vtf.getMipCount(), 9); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); @@ -811,7 +811,7 @@ TEST(vtfpp, write_v75) { EXPECT_EQ(vtf.getHeight(), 256); EXPECT_EQ(vtf.getFlags(), 0); EXPECT_EQ(vtf.getFormat(), ImageFormat::DXT1); - EXPECT_EQ(vtf.getMipCount(), 7); + EXPECT_EQ(vtf.getMipCount(), 9); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); @@ -1476,7 +1476,7 @@ TEST(vtfpp, write_v76_c6) { EXPECT_EQ(vtf.getHeight(), 256); EXPECT_EQ(vtf.getFlags(), VTF::FLAG_V0_MULTI_BIT_ALPHA); EXPECT_EQ(vtf.getFormat(), ImageFormat::BC7); - EXPECT_EQ(vtf.getMipCount(), 7); + EXPECT_EQ(vtf.getMipCount(), 9); EXPECT_EQ(vtf.getFrameCount(), 1); EXPECT_EQ(vtf.getFaceCount(), 1); EXPECT_EQ(vtf.getDepth(), 1); From 4700b97e32d19db4bead83782b4c756beffc049f Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 23 Oct 2025 06:15:41 -0700 Subject: [PATCH 02/17] vtfpp: enable non-po2 textures to have mips to 1x1, compressed textures to have non-multiple-of-4 mips, properly support mipmapped depth textures --- include/vtfpp/ImageFormats.h | 78 ++++++++++++++++++------- include/vtfpp/VTF.h | 2 +- lang/c/include/vtfppc/ImageFormats.h | 2 +- lang/c/src/vtfppc/ImageFormats.cpp | 4 +- lang/python/src/vtfpp.h | 3 +- src/vtfpp/ImageConversion.cpp | 8 ++- src/vtfpp/VTF.cpp | 87 ++++++++++++++++------------ 7 files changed, 117 insertions(+), 67 deletions(-) diff --git a/include/vtfpp/ImageFormats.h b/include/vtfpp/ImageFormats.h index de7306368..a3ad03867 100644 --- a/include/vtfpp/ImageFormats.h +++ b/include/vtfpp/ImageFormats.h @@ -1,5 +1,11 @@ +// ReSharper disable CppDFAUnreachableFunctionCall +// ReSharper disable CppRedundantParentheses +// ReSharper disable CppRedundantQualifier + #pragma once +#include + #include namespace vtfpp { @@ -647,13 +653,44 @@ namespace ImageFormatDetails { namespace ImageDimensions { -[[nodiscard]] constexpr uint32_t getMipDim(uint8_t mip, uint16_t dim) { +[[nodiscard]] constexpr uint16_t getMipDim(uint8_t mip, bool addCompressedFormatPadding, uint16_t dim) { + if (!dim) { + dim = 1; + } for (int i = 0; i < mip && dim > 1; i++) { - dim /= 2; + dim >>= 1; + } + if (addCompressedFormatPadding) { + dim += sourcepp::math::paddingForAlignment(4, dim); } return dim; } +[[nodiscard]] constexpr std::pair getMipDims(uint8_t mip, bool addCompressedFormatPadding, uint16_t width, uint16_t height) { + for (int i = 0; i < mip && (width > 1 || height > 1); i++) { + if ((width >>= 1) < 1) width = 1; + if ((height >>= 1) < 1) height = 1; + } + if (addCompressedFormatPadding) { + width += sourcepp::math::paddingForAlignment(4, width); + height += sourcepp::math::paddingForAlignment(4, height); + } + return {width, height}; +} + +[[nodiscard]] constexpr std::tuple getMipDims(uint8_t mip, bool addCompressedFormatPadding, uint16_t width, uint16_t height, uint16_t depth) { + for (int i = 0; i < mip && (width > 1 || height > 1 || depth > 1); i++) { + if ((width >>= 1) < 1) width = 1; + if ((height >>= 1) < 1) height = 1; + if ((depth >>= 1) < 1) depth = 1; + } + if (addCompressedFormatPadding) { + width += sourcepp::math::paddingForAlignment(4, width); + height += sourcepp::math::paddingForAlignment(4, height); + } + return {width, height, depth}; +} + [[nodiscard]] constexpr uint8_t getMaximumMipCount(uint16_t width, uint16_t height, uint16_t depth = 1) { uint8_t numMipLevels = 1; if (width && height && depth) { @@ -672,37 +709,30 @@ namespace ImageDimensions { namespace ImageFormatDetails { [[nodiscard]] constexpr uint32_t getDataLength(ImageFormat format, uint16_t width, uint16_t height, uint16_t depth = 1) { - switch(format) { - using enum ImageFormat; - case DXT3: - case DXT5: - case ATI2N: - case BC7: - case BC6H: - case ATI1N: - case DXT1: - case DXT1_ONE_BIT_ALPHA: - return ((((width + 3) / 4) < 1) ? 1 : ((width + 3) / 4)) * ((((height + 3) / 4) < 1) ? 1 : ((height + 3) / 4)) * depth * bpp(format) * 2; - default: - break; + if (ImageFormatDetails::compressed(format)) { + return ((width + 3) / 4) * ((height + 3) / 4) * depth * bpp(format) * 2; } return width * height * depth * (bpp(format) / 8); } [[nodiscard]] constexpr uint32_t getDataLength(ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth = 1) { + const bool compressed = ImageFormatDetails::compressed(format); uint32_t length = 0; - for (int mip = mipCount - 1; mip >= 0; mip--) { - length += ImageFormatDetails::getDataLength(format, ImageDimensions::getMipDim(mip, width), ImageDimensions::getMipDim(mip, height), depth) * frameCount * faceCount; + for (int i = mipCount - 1; i >= 0; i--) { + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, compressed, width, height, depth); + length += ImageFormatDetails::getDataLength(format, mipWidth, mipHeight, mipDepth) * frameCount * faceCount; } 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 depth = 1) { + const bool compressed = ImageFormatDetails::compressed(format); 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), depth) * faceCount; + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, compressed, width, height, depth); + length += ImageFormatDetails::getDataLength(format, mipWidth, mipHeight, mipDepth) * faceCount; } if (padded && j + 1 != frameCount && length > 512) { length += sourcepp::math::paddingForAlignment(512, length); @@ -712,13 +742,15 @@ namespace ImageFormatDetails { } [[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 depth = 1) { + const bool compressed = ImageFormatDetails::compressed(format); offset = 0; length = 0; for (int i = mipCount - 1; i >= 0; i--) { + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, compressed, width, height, depth); for (int j = 0; j < frameCount; j++) { for (int k = 0; k < faceCount; k++) { - for (int l = 0; l < depth; l++) { - const auto imageSize = ImageFormatDetails::getDataLength(format, ImageDimensions::getMipDim(i, width), ImageDimensions::getMipDim(i, height)); + for (int l = 0; l < mipDepth; l++) { + const auto imageSize = ImageFormatDetails::getDataLength(format, mipWidth, mipHeight); if (i == mip && j == frame && k == face && l == slice) { length = imageSize; return true; @@ -733,13 +765,15 @@ namespace ImageFormatDetails { // 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 depth = 1) { + const bool compressed = ImageFormatDetails::compressed(format); 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 < depth; l++) { - const auto imageSize = ImageFormatDetails::getDataLength(format, ImageDimensions::getMipDim(i, width), ImageDimensions::getMipDim(i, height)); + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, compressed, width, height, depth); + for (int l = 0; l < mipDepth; l++) { + const auto imageSize = ImageFormatDetails::getDataLength(format, mipWidth, mipHeight); if (i == mip && j == frame && k == face && l == slice) { length = imageSize; return true; diff --git a/include/vtfpp/VTF.h b/include/vtfpp/VTF.h index 018de9808..462721e3d 100644 --- a/include/vtfpp/VTF.h +++ b/include/vtfpp/VTF.h @@ -365,7 +365,7 @@ class VTF { bool setFaceCount(bool isCubeMap); - [[nodiscard]] uint16_t getDepth() const; + [[nodiscard]] uint16_t getDepth(uint8_t mip = 0) const; bool setDepth(uint16_t newDepth); diff --git a/lang/c/include/vtfppc/ImageFormats.h b/lang/c/include/vtfppc/ImageFormats.h index c980dabad..7dcd84bad 100644 --- a/lang/c/include/vtfppc/ImageFormats.h +++ b/lang/c/include/vtfppc/ImageFormats.h @@ -102,7 +102,7 @@ SOURCEPP_API int vtfpp_image_format_details_opaque(vtfpp_image_format_e format); SOURCEPP_API int vtfpp_image_format_details_console(vtfpp_image_format_e format); -SOURCEPP_API uint32_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, uint16_t dim); +SOURCEPP_API uint16_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, int add_compressed_format_padding, uint16_t dim); SOURCEPP_API uint8_t vtfpp_image_dimensions_get_maximum_mip_count(uint16_t width, uint16_t height, uint16_t depth); diff --git a/lang/c/src/vtfppc/ImageFormats.cpp b/lang/c/src/vtfppc/ImageFormats.cpp index 44250b004..37492527e 100644 --- a/lang/c/src/vtfppc/ImageFormats.cpp +++ b/lang/c/src/vtfppc/ImageFormats.cpp @@ -70,8 +70,8 @@ SOURCEPP_API int vtfpp_image_format_details_console(vtfpp_image_format_e format) return ImageFormatDetails::console(static_cast(format)); } -SOURCEPP_API uint32_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, uint16_t dim) { - return ImageDimensions::getMipDim(mip, dim); +SOURCEPP_API uint16_t vtfpp_image_dimensions_get_mip_dim(uint8_t mip, int add_compressed_format_padding, uint16_t dim) { + return ImageDimensions::getMipDim(mip, add_compressed_format_padding, dim); } SOURCEPP_API uint8_t vtfpp_image_dimensions_get_maximum_mip_count(uint16_t width, uint16_t height, uint16_t depth) { diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h index 0ad7c9315..76951d749 100644 --- a/lang/python/src/vtfpp.h +++ b/lang/python/src/vtfpp.h @@ -141,7 +141,8 @@ inline void register_python(py::module_& m) { auto ImageDimensions = vtfpp.def_submodule("ImageDimensions"); ImageDimensions - .def("get_mip_dim", &getMipDim, "mip"_a, "dim"_a) + .def("get_mip_dim", &getMipDim, "mip"_a, "add_compressed_format_padding"_a, "dim"_a) + .def("get_mip_dims", static_cast(*)(uint8_t, bool, uint16_t, uint16_t, uint16_t)>(&getMipDims), "mip"_a, "add_compressed_format_padding"_a, "width"_a, "height"_a, "depth"_a = 1) .def("get_maximum_mip_count", &getMaximumMipCount, "width"_a, "height"_a, "depth"_a = 1); } diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index 873519475..95a65f7c6 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -943,13 +943,15 @@ std::vector ImageConversion::convertSeveralImageDataToFormat(std::spa return {imageData.begin(), imageData.end()}; } + const bool compressed = ImageFormatDetails::compressed(oldFormat); std::vector out(ImageFormatDetails::getDataLength(newFormat, mipCount, frameCount, faceCount, width, height, depth)); for(int mip = mipCount - 1; mip >= 0; mip--) { + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(mip, compressed, width, height, depth); for (int frame = 0; frame < frameCount; frame++) { for (int face = 0; face < faceCount; face++) { - for (int slice = 0; slice < depth; slice++) { + for (int slice = 0; slice < mipDepth; slice++) { if (uint32_t oldOffset, oldLength; ImageFormatDetails::getDataPosition(oldOffset, oldLength, oldFormat, mip, mipCount, frame, frameCount, face, faceCount, width, height, slice, depth)) { - const auto convertedImageData = ImageConversion::convertImageDataToFormat({imageData.data() + oldOffset, oldLength}, oldFormat, newFormat, ImageDimensions::getMipDim(mip, width), ImageDimensions::getMipDim(mip, height), quality); + const auto convertedImageData = ImageConversion::convertImageDataToFormat({imageData.data() + oldOffset, oldLength}, oldFormat, newFormat, mipWidth, mipHeight, quality); if (uint32_t newOffset, newLength; ImageFormatDetails::getDataPosition(newOffset, newLength, newFormat, mip, mipCount, frame, frameCount, face, faceCount, width, height, slice, depth) && newLength == convertedImageData.size()) { std::memcpy(out.data() + newOffset, convertedImageData.data(), newLength); } @@ -1966,7 +1968,7 @@ std::vector ImageConversion::resizeImageData(std::span(math::kaiserWindow(x * s, KAISER_BETA(s))); }; static constexpr auto KAISER_SUPPORT = [](float s, void*) -> float { - float baseSupport = KAISER_BETA(s) / 2.f; + const float baseSupport = KAISER_BETA(s) / 2.f; if (s > 1.f) { return std::max(2.f, baseSupport - 0.5f); } diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index 2b37840cf..c0ef216fc 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -98,7 +98,7 @@ constexpr void swizzleUncompressedImageData(std::span inputData, std: offset |= (x & 1) << shiftCount++; x >>= 1; } - } while (x | y | z); + } while (x || y || z); return offset; }; @@ -156,21 +156,21 @@ void swapImageDataEndianForConsole(std::span imageData, ImageFormat f // 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)))) { + const bool compressed = ImageFormatDetails::compressed(format); std::vector out(imageData.size()); for(int mip = mipCount - 1; mip >= 0; mip--) { - const auto mipWidth = ImageDimensions::getMipDim(mip, width); - const auto mipHeight = ImageDimensions::getMipDim(mip, height); + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(mip, compressed, width, height, depth); for (int frame = 0; frame < frameCount; frame++) { for (int face = 0; face < faceCount; face++) { 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 * depth}; - std::span outSpan{out.data() + offset, length * depth}; + std::span imageDataSpan{imageData.data() + offset, length * mipDepth}; + std::span outSpan{out.data() + offset, length * mipDepth}; if (ImageFormatDetails::bpp(format) % 32 == 0) { - ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, depth); + ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth); } else if (ImageFormatDetails::bpp(format) % 16 == 0) { - ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, depth); + ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth); } else /*if (ImageFormatDetails::bpp(format) % 8 == 0)*/ { - ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, depth); + ::swizzleUncompressedImageData(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth); } } } @@ -188,9 +188,10 @@ template if constexpr (ConvertingFromDDS) { for (int i = mipCount - 1; i >= 0; i--) { + const auto mipDepth = ImageDimensions::getMipDim(i, false, depth); for (int j = 0; j < frameCount; j++) { for (int k = 0; k < faceCount; k++) { - for (int l = 0; l < depth; l++) { + for (int l = 0; l < mipDepth; l++) { uint32_t oldOffset, length; if (!ImageFormatDetails::getDataPositionXbox(oldOffset, length, padded, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, depth)) { ok = false; @@ -205,7 +206,8 @@ template 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 < depth; l++) { + const auto mipDepth = ImageDimensions::getMipDim(i, false, depth); + for (int l = 0; l < mipDepth; l++) { uint32_t oldOffset, length; if (!ImageFormatDetails::getDataPosition(oldOffset, length, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, depth)) { ok = false; @@ -981,11 +983,11 @@ void VTF::setImageHeightResizeMethod(ImageConversion::ResizeMethod imageHeightRe } uint16_t VTF::getWidth(uint8_t mip) const { - return mip > 0 ? ImageDimensions::getMipDim(mip, this->width) : this->width; + return ImageDimensions::getMipDim(mip, ImageFormatDetails::compressed(this->format), this->width); } uint16_t VTF::getHeight(uint8_t mip) const { - return mip > 0 ? ImageDimensions::getMipDim(mip, this->height) : this->height; + return ImageDimensions::getMipDim(mip, ImageFormatDetails::compressed(this->format), this->height); } void VTF::setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter) { @@ -1147,6 +1149,7 @@ void VTF::computeMips(ImageConversion::ResizeFilter filter) { auto* outputDataPtr = imageResource->data.data(); const auto faceCount = this->getFaceCount(); + const bool compressed = ImageFormatDetails::compressed(this->format); #ifdef SOURCEPP_BUILD_WITH_THREADS std::vector> futures; @@ -1154,26 +1157,28 @@ void VTF::computeMips(ImageConversion::ResizeFilter filter) { #endif for (int j = 0; j < this->frameCount; j++) { for (int k = 0; k < faceCount; k++) { - for (int l = 0; l < this->depth; l++) { #ifdef SOURCEPP_BUILD_WITH_THREADS - futures.push_back(std::async(std::launch::async, [this, filter, outputDataPtr, faceCount, j, k, l] { + futures.push_back(std::async(std::launch::async, [this, filter, outputDataPtr, faceCount, compressed, j, k] { #endif - for (int i = 1; i < this->mipCount; i++) { - auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(i - 1, j, k, l), this->format, ImageDimensions::getMipDim(i - 1, this->width), ImageDimensions::getMipDim(i, this->width), ImageDimensions::getMipDim(i - 1, this->height), ImageDimensions::getMipDim(i, this->height), this->isSRGB(), filter); + for (int i = 1; i < this->mipCount; i++) { + const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, compressed, this->width, this->height, this->depth); + const auto [mipWidthM1, mipHeightM1] = ImageDimensions::getMipDims(i, compressed, this->width, this->height); + for (int l = 0; l < mipDepth; l++) { + auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(i - 1, j, k, l), this->format, mipWidthM1, mipWidth, mipHeightM1, mipHeight, this->isSRGB(), filter); if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, l, this->depth) && mip.size() == length) { std::memcpy(outputDataPtr + offset, mip.data(), length); } } + } #ifdef SOURCEPP_BUILD_WITH_THREADS - })); - if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) { - for (auto& future : futures) { - future.get(); - } - futures.clear(); + })); + if (futures.size() >= std::thread::hardware_concurrency()) { + for (auto& future : futures) { + future.get(); } -#endif + futures.clear(); } +#endif } } #ifdef SOURCEPP_BUILD_WITH_THREADS @@ -1229,8 +1234,8 @@ bool VTF::setFaceCount(bool isCubemap) { return true; } -uint16_t VTF::getDepth() const { - return this->depth; +uint16_t VTF::getDepth(uint8_t mip) const { + return ImageDimensions::getMipDim(mip, false, this->depth); } bool VTF::setDepth(uint16_t newDepth) { @@ -1241,11 +1246,11 @@ bool VTF::setDepth(uint16_t newDepth) { return true; } -bool VTF::setFrameFaceAndDepth(uint16_t newFrameCount, bool isCubemap, uint16_t newDepth) { +bool VTF::setFrameFaceAndDepth(uint16_t newFrameCount, bool isCubeMap, uint16_t newDepth) { if (!this->hasImageData()) { return false; } - this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, isCubemap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, newDepth); + this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, isCubeMap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, newDepth); return true; } @@ -1299,7 +1304,7 @@ void VTF::computeReflectivity() { futures.push_back(std::async(std::launch::async, [this, j, k, l] { return getReflectivityForImage(*this, j, k, l); })); - if (std::thread::hardware_concurrency() > 0 && futures.size() >= std::thread::hardware_concurrency()) { + if (futures.size() >= std::thread::hardware_concurrency()) { for (auto& future : futures) { this->reflectivity += future.get(); } @@ -1461,6 +1466,8 @@ void VTF::regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t this->flags |= FLAG_V0_NO_MIP | FLAG_V0_NO_LOD; } + const bool compressed = ImageFormatDetails::compressed(this->format); + const bool newCompressed = ImageFormatDetails::compressed(newFormat); std::vector newImageData; if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) { if (this->format != newFormat && this->width == newWidth && this->height == newHeight && this->mipCount == newMipCount && this->frameCount == newFrameCount && faceCount == newFaceCount && this->depth == newDepth) { @@ -1468,14 +1475,16 @@ void VTF::regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t } else { newImageData.resize(ImageFormatDetails::getDataLength(this->format, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newDepth)); for (int i = newMipCount - 1; i >= 0; i--) { + const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(i, compressed, this->width, this->height); + const auto [newMipWidth, newMipHeight, newMipDepth] = ImageDimensions::getMipDims(i, newCompressed, newWidth, newHeight, newDepth); for (int j = 0; j < newFrameCount; j++) { for (int k = 0; k < newFaceCount; k++) { - for (int l = 0; l < newDepth; l++) { + for (int l = 0; l < newMipDepth; l++) { if (i < this->mipCount && j < this->frameCount && k < faceCount && l < this->depth) { auto imageSpan = this->getImageDataRaw(i, j, k, l); std::vector image{imageSpan.begin(), imageSpan.end()}; if (this->width != newWidth || this->height != newHeight) { - image = ImageConversion::resizeImageData(image, this->format, ImageDimensions::getMipDim(i, this->width), ImageDimensions::getMipDim(i, newWidth), ImageDimensions::getMipDim(i, this->height), ImageDimensions::getMipDim(i, newHeight), this->isSRGB(), filter); + image = ImageConversion::resizeImageData(image, this->format, mipWidth, newMipWidth, mipHeight, newMipHeight, this->isSRGB(), filter); } if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, newMipCount, j, newFrameCount, k, newFaceCount, newWidth, newHeight, l, newDepth) && image.size() == length) { std::memcpy(newImageData.data() + offset, image.data(), length); @@ -1694,10 +1703,12 @@ std::vector VTF::getImageDataAs(ImageFormat newFormat, uint8_t mip, u } if (this->format == ImageFormat::P8) { if (const auto paletteData = this->getPaletteResourceFrame(frame); !paletteData.empty()) { - return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(paletteData, rawImageData), ImageFormat::BGRA8888, newFormat, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height)); + const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, ImageFormatDetails::compressed(ImageFormat::BGRA8888), this->width, this->height); + return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(paletteData, rawImageData), ImageFormat::BGRA8888, newFormat, mipWidth, mipHeight); } } - return ImageConversion::convertImageDataToFormat(rawImageData, this->format, newFormat, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height)); + const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, ImageFormatDetails::compressed(this->format), this->width, this->height); + return ImageConversion::convertImageDataToFormat(rawImageData, this->format, newFormat, mipWidth, mipHeight); } std::vector VTF::getImageDataAsRGBA8888(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const { @@ -1736,8 +1747,7 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u } if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, faceCount, this->width, this->height, slice, this->depth)) { std::vector image{imageData_.begin(), imageData_.end()}; - const auto newWidth = ImageDimensions::getMipDim(mip, this->width); - const auto newHeight = ImageDimensions::getMipDim(mip, this->height); + const auto [newWidth, newHeight] = ImageDimensions::getMipDims(mip, ImageFormatDetails::compressed(this->format), this->width, this->height); if (width_ != newWidth || height_ != newHeight) { image = ImageConversion::resizeImageData(image, format_, width_, newWidth, height_, newHeight, this->isSRGB(), filter); } @@ -1780,7 +1790,7 @@ bool VTF::setImage(const std::string& imagePath, ImageConversion::ResizeFilter f } std::vector VTF::saveImageToFile(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const { - return ImageConversion::convertImageDataToFile(this->getImageDataRaw(mip, frame, face, slice), this->format, ImageDimensions::getMipDim(mip, this->width), ImageDimensions::getMipDim(mip, this->height), fileFormat); + return ImageConversion::convertImageDataToFile(this->getImageDataRaw(mip, frame, face, slice), this->format, this->getWidth(mip), this->getHeight(mip), fileFormat); } bool VTF::saveImageToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const { @@ -1914,6 +1924,7 @@ void VTF::computeFallback(ImageConversion::ResizeFilter filter) { } const auto faceCount = this->getFaceCount(); + const bool compressed = ImageFormatDetails::compressed(this->format); this->fallbackWidth = 8; this->fallbackHeight = 8; @@ -1922,9 +1933,10 @@ void VTF::computeFallback(ImageConversion::ResizeFilter filter) { 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--) { + const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(i, compressed, this->fallbackWidth, this->fallbackHeight); 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); + auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(0, j, k, 0), this->format, this->width, mipWidth, this->height, mipHeight, 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); } @@ -1942,7 +1954,8 @@ void VTF::removeFallback() { } 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); + const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, ImageFormatDetails::compressed(this->format), this->fallbackWidth, this->fallbackHeight); + return ImageConversion::convertImageDataToFile(this->getFallbackDataRaw(mip, frame, face), this->format, mipWidth, mipHeight, fileFormat); } bool VTF::saveFallbackToFile(const std::string& imagePath, uint8_t mip, uint16_t frame, uint8_t face, ImageConversion::FileFormat fileFormat) const { From a3577c9b5fc12bbd8c19cbe8512307c4caa49077 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 23 Oct 2025 07:43:51 -0700 Subject: [PATCH 03/17] vtfpp: document ImageFormats.h --- include/vtfpp/ImageFormats.h | 199 ++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/include/vtfpp/ImageFormats.h b/include/vtfpp/ImageFormats.h index a3ad03867..95cfcf6ef 100644 --- a/include/vtfpp/ImageFormats.h +++ b/include/vtfpp/ImageFormats.h @@ -80,6 +80,12 @@ enum class ImageFormat : int32_t { namespace ImageFormatDetails { +/** + * Get the number of bits of precision of the red channel in the given format. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the red channel has in the given format. -1 + * is returned if the given format is compressed. + */ [[nodiscard]] constexpr int8_t red(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -149,6 +155,11 @@ namespace ImageFormatDetails { return 0; } +/** + * Get the number of bits of precision of the red channel in the given format regardless of compression. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the red channel has in the given format. + */ [[nodiscard]] constexpr int8_t decompressedRed(ImageFormat format) { // This is merely for convenience, the true size may be different depending on the data switch (format) { @@ -169,6 +180,12 @@ namespace ImageFormatDetails { return red(format); } +/** + * Get the number of bits of precision of the green channel in the given format. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the green channel has in the given format. -1 + * is returned if the given format is compressed. + */ [[nodiscard]] constexpr int8_t green(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -239,6 +256,11 @@ namespace ImageFormatDetails { return 0; } +/** + * Get the number of bits of precision of the green channel in the given format regardless of compression. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the green channel has in the given format. + */ [[nodiscard]] constexpr int8_t decompressedGreen(ImageFormat format) { // This is merely for convenience, the true size may be different depending on the data switch (format) { @@ -259,6 +281,12 @@ namespace ImageFormatDetails { return green(format); } +/** + * Get the number of bits of precision of the blue channel in the given format. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the blue channel has in the given format. -1 + * is returned if the given format is compressed. + */ [[nodiscard]] constexpr int8_t blue(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -328,6 +356,11 @@ namespace ImageFormatDetails { return 0; } +/** + * Get the number of bits of precision of the blue channel in the given format regardless of compression. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the blue channel has in the given format. + */ [[nodiscard]] constexpr int8_t decompressedBlue(ImageFormat format) { // This is merely for convenience, the true size may be different depending on the data switch (format) { @@ -348,6 +381,12 @@ namespace ImageFormatDetails { return blue(format); } +/** + * Get the number of bits of precision of the alpha channel in the given format. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the alpha channel has in the given format. -1 + * is returned if the given format is compressed. + */ [[nodiscard]] constexpr int8_t alpha(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -417,6 +456,11 @@ namespace ImageFormatDetails { return 0; } +/** + * Get the number of bits of precision of the alpha channel in the given format regardless of compression. + * @param format The format to get the number of bits of precision in this channel from. + * @return The number of bits of precision the alpha channel has in the given format. + */ [[nodiscard]] constexpr int8_t decompressedAlpha(ImageFormat format) { // This is merely for convenience, the true size may be different depending on the data switch (format) { @@ -439,6 +483,12 @@ namespace ImageFormatDetails { return alpha(format); } +/** + * Find the bits per pixel of the given format. Note this is bits per pixel, not bytes + * per pixel. The DXT1 format is 0.5 bytes per pixel, which cannot be stored in a uint8_t. + * @param format The format to find the bits per pixel of. + * @return The bits per pixel of the given format. + */ [[nodiscard]] constexpr uint8_t bpp(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -509,6 +559,14 @@ namespace ImageFormatDetails { return 0; } +/** + * Find a container format for the given format, a format that is more commonly understood + * within this library and can represent the input texture without losing data. + * @param format The format to find the container format of. + * @return The container format of the given format. If the given format is representable with + * RGBA8888, that is used. RGBA16161616 is used if the given format is representable with that. + * RGBA32323232F is used for formats that are too large for both the previous two. + */ [[nodiscard]] constexpr ImageFormat containerFormat(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -574,18 +632,38 @@ namespace ImageFormatDetails { return ImageFormat::EMPTY; } +/** + * Check if the given format is representable by RGBA8888 without losing data. + * @param format The format to check. + * @return True if the given format cannot be represented by RGBA8888 without losing data. + */ [[nodiscard]] constexpr bool large(ImageFormat format) { return containerFormat(format) != ImageFormat::RGBA8888 && containerFormat(format) != ImageFormat::EMPTY; } +/** + * Checks if the given format stores floating points in its channels. + * @param format The format to check. + * @return True if the given format stores floating points. + */ [[nodiscard]] constexpr bool decimal(ImageFormat format) { return containerFormat(format) == ImageFormat::RGBA32323232F; } +/** + * Check if the given format is a compressed format (DXT1, DXT3, DXT5, ATI1N, ATI2N, BC7, BC6H). + * @param format The format to check. + * @return True if the given format is compressed. + */ [[nodiscard]] constexpr bool compressed(ImageFormat format) { return red(format) == -1; } +/** + * Check if the given format can store transparency. + * @param format The format to check. + * @return True if the format can store transparency. + */ [[nodiscard]] constexpr bool transparent(ImageFormat format) { const auto a = alpha(format); if (a < 0) { @@ -623,10 +701,20 @@ namespace ImageFormatDetails { return a != 0; } +/** + * Check if the given format cannot store transparency. + * @param format The format to check. + * @return True if the format cannot store transparency. + */ [[nodiscard]] constexpr bool opaque(ImageFormat format) { return !transparent(format); } +/** + * Check if the given format is exclusively used on console platforms. + * @param format The format to check. + * @return True if the format is exclusively used on console platforms. + */ [[nodiscard]] constexpr bool console(ImageFormat format) { switch (format) { using enum ImageFormat; @@ -653,6 +741,15 @@ namespace ImageFormatDetails { namespace ImageDimensions { +/** + * Get the dimension at a given mip level. + * @param mip The mip level. + * @param addCompressedFormatPadding Aligns the output dimension to 4 pixels. + * This should not be enabled if the input dimension is depth, as compressed formats are compressed on 2D slices. + * Otherwise, it should be enabled if ImageFormatDetails::compressed(format) is true. + * @param dim The dimension of the largest mip in the texture. Can be width, height, or depth. + * @return The dimension at the given mip level. + */ [[nodiscard]] constexpr uint16_t getMipDim(uint8_t mip, bool addCompressedFormatPadding, uint16_t dim) { if (!dim) { dim = 1; @@ -666,6 +763,14 @@ namespace ImageDimensions { return dim; } +/** + * Get the width and height at a given mip level. + * @param mip The mip level. + * @param addCompressedFormatPadding Aligns the output width and height to 4 pixels. + * @param width The width of the largest mip in the texture. + * @param height The height of the largest mip in the texture. + * @return The width and height at the given mip level. + */ [[nodiscard]] constexpr std::pair getMipDims(uint8_t mip, bool addCompressedFormatPadding, uint16_t width, uint16_t height) { for (int i = 0; i < mip && (width > 1 || height > 1); i++) { if ((width >>= 1) < 1) width = 1; @@ -678,6 +783,15 @@ namespace ImageDimensions { return {width, height}; } +/** + * Get the width, height, and depth at a given mip level. + * @param mip The mip level. + * @param addCompressedFormatPadding Aligns the output width and height to 4 pixels. + * @param width The width of the largest mip in the texture. + * @param height The height of the largest mip in the texture. + * @param depth The depth of the largest mip in the texture. + * @return The width, height, and depth at the given mip level. + */ [[nodiscard]] constexpr std::tuple getMipDims(uint8_t mip, bool addCompressedFormatPadding, uint16_t width, uint16_t height, uint16_t depth) { for (int i = 0; i < mip && (width > 1 || height > 1 || depth > 1); i++) { if ((width >>= 1) < 1) width = 1; @@ -691,6 +805,15 @@ namespace ImageDimensions { return {width, height, depth}; } +/** + * Calculate the largest mip count a texture with the given width, height, and depth + * can contain. On console platforms, since there is no mip count field in the header, + * this determines the actual mip count if the texture contains mips. + * @param width The width of the largest mip in the texture. + * @param height The height of the largest mip in the texture. + * @param depth The depth of the largest mip in the texture. + * @return The largest mip count possible for the texture to contain. + */ [[nodiscard]] constexpr uint8_t getMaximumMipCount(uint16_t width, uint16_t height, uint16_t depth = 1) { uint8_t numMipLevels = 1; if (width && height && depth) { @@ -708,6 +831,14 @@ namespace ImageDimensions { namespace ImageFormatDetails { +/** + * Calculate the amount of data required to store a texture with the given format and dimensions. + * @param format The format of the texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param depth The depth of the texture. + * @return The length in bytes of a texture containing the given format, width, height, and depth. + */ [[nodiscard]] constexpr uint32_t getDataLength(ImageFormat format, uint16_t width, uint16_t height, uint16_t depth = 1) { if (ImageFormatDetails::compressed(format)) { return ((width + 3) / 4) * ((height + 3) / 4) * depth * bpp(format) * 2; @@ -715,6 +846,18 @@ namespace ImageFormatDetails { return width * height * depth * (bpp(format) / 8); } +/** + * Calculate the amount of data required to store a texture with the given format, mip/frame/face count, and dimensions. + * @param format The format of the texture. + * @param mipCount The mip count of the texture. + * @param frameCount The frame count of the texture. + * @param faceCount The face count of the texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param depth The depth of the texture. + * @return The length in bytes of a texture containing the given format, mip count, frame count, + * face count, width, height, and depth. + */ [[nodiscard]] constexpr uint32_t getDataLength(ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth = 1) { const bool compressed = ImageFormatDetails::compressed(format); uint32_t length = 0; @@ -725,7 +868,22 @@ namespace ImageFormatDetails { return length; } -// XTF (PLATFORM_XBOX) has padding between frames to align each one to 512 bytes +/** + * Calculate the amount of data required to store an XBOX platform texture with the given format, mip/frame/face count, + * and dimensions. XBOX platform textures have padding between frames to attempt to align each one to 512 bytes (this + * explanation is simplified from the actual padding algorithm but is mostly correct). + * @param padded Whether to add padding after some frames. Some parts of an XBOX platform texture + * expect this padding and some do not. + * @param format The format of the texture. + * @param mipCount The mip count of the texture. + * @param frameCount The frame count of the texture. + * @param faceCount The face count of the texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param depth The depth of the texture. + * @return The length in bytes of an XBOX platform texture containing the given format, mip count, + * frame count, face count, width, height, and depth; possibly with padding after some frames. + */ [[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 depth = 1) { const bool compressed = ImageFormatDetails::compressed(format); uint32_t length = 0; @@ -741,6 +899,23 @@ namespace ImageFormatDetails { return length; } +/** + * Find the position of a specific mip, frame, face, and slice within a texture. + * @param offset Set to the offset of the section of the texture being searched for. + * @param length Set to the length of the section of the texture being searched for. + * @param format The format of the texture. + * @param mip The mip level to search for within the texture. + * @param mipCount The mip count of the texture. + * @param frame The frame to search for within the texture. + * @param frameCount The frame count of the texture. + * @param face The face to search for within the texture. + * @param faceCount The face count of the texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param slice The slice to search for within the texture. + * @param depth The depth of the texture. + * @return True if the section of the texture was successfully found. + */ [[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 depth = 1) { const bool compressed = ImageFormatDetails::compressed(format); offset = 0; @@ -763,7 +938,27 @@ namespace ImageFormatDetails { return false; } -// XTF (PLATFORM_XBOX) is more like DDS layout +/** + * Find the position of a specific mip, frame, face, and slice within an XBOX platform texture. + * XBOX platform textures are laid out in the inverse of a regular texture in DDS mip layout and + * have padding between some frames. + * @param offset Set to the offset of the section of the texture being searched for. + * @param length Set to the length of the section of the texture being searched for. + * @param padded Whether to add padding after some frames. Some parts of an XBOX platform texture + * expect this padding and some do not. + * @param format The format of the texture. + * @param mip The mip level to search for within the texture. + * @param mipCount The mip count of the texture. + * @param frame The frame to search for within the texture. + * @param frameCount The frame count of the texture. + * @param face The face to search for within the texture. + * @param faceCount The face count of the texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param slice The slice to search for within the texture. + * @param depth The depth of the texture. + * @return True if the section of the texture was successfully found. + */ [[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 depth = 1) { const bool compressed = ImageFormatDetails::compressed(format); offset = 0; From 109d06edd35dc020b7ede01f6d9da2b0e5e41ec1 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Thu, 23 Oct 2025 22:52:20 -0700 Subject: [PATCH 04/17] vtfpp: add accessor methods to get unpadded width/height --- include/vtfpp/VTF.h | 4 ++++ lang/c/include/vtfppc/VTF.h | 4 ++++ lang/c/src/vtfppc/VTF.cpp | 12 ++++++++++++ lang/python/src/vtfpp.h | 4 ++++ src/vtfpp/VTF.cpp | 8 ++++++++ 5 files changed, 32 insertions(+) diff --git a/include/vtfpp/VTF.h b/include/vtfpp/VTF.h index 462721e3d..5487c9662 100644 --- a/include/vtfpp/VTF.h +++ b/include/vtfpp/VTF.h @@ -325,8 +325,12 @@ class VTF { [[nodiscard]] uint16_t getWidth(uint8_t mip = 0) const; + [[nodiscard]] uint16_t getWidthWithoutPadding(uint8_t mip = 0) const; + [[nodiscard]] uint16_t getHeight(uint8_t mip = 0) const; + [[nodiscard]] uint16_t getHeightWithoutPadding(uint8_t mip = 0) const; + void setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter); [[nodiscard]] uint32_t getFlags() const; diff --git a/lang/c/include/vtfppc/VTF.h b/lang/c/include/vtfppc/VTF.h index 6578972bb..ff299c75c 100644 --- a/lang/c/include/vtfppc/VTF.h +++ b/lang/c/include/vtfppc/VTF.h @@ -246,8 +246,12 @@ SOURCEPP_API void vtfpp_vtf_set_image_height_resize_method(vtfpp_vtf_handle_t ha SOURCEPP_API uint16_t vtfpp_vtf_get_width(vtfpp_vtf_handle_t handle, uint8_t mip); +SOURCEPP_API uint16_t vtfpp_vtf_get_width_without_padding(vtfpp_vtf_handle_t handle, uint8_t mip); + SOURCEPP_API uint16_t vtfpp_vtf_get_height(vtfpp_vtf_handle_t handle, uint8_t mip); +SOURCEPP_API uint16_t vtfpp_vtf_get_height_without_padding(vtfpp_vtf_handle_t handle, uint8_t mip); + SOURCEPP_API void vtfpp_vtf_set_size(vtfpp_vtf_handle_t handle, uint16_t width, uint16_t height, vtfpp_image_conversion_resize_filter_e filter); SOURCEPP_API uint32_t vtfpp_vtf_get_flags(vtfpp_vtf_handle_t handle); diff --git a/lang/c/src/vtfppc/VTF.cpp b/lang/c/src/vtfppc/VTF.cpp index 2e4a6ba43..382112ee1 100644 --- a/lang/c/src/vtfppc/VTF.cpp +++ b/lang/c/src/vtfppc/VTF.cpp @@ -218,12 +218,24 @@ SOURCEPP_API uint16_t vtfpp_vtf_get_width(vtfpp_vtf_handle_t handle, uint8_t mip return Convert::vtf(handle)->getWidth(mip); } +SOURCEPP_API uint16_t vtfpp_vtf_get_width_without_padding(vtfpp_vtf_handle_t handle, uint8_t mip) { + SOURCEPP_EARLY_RETURN_VAL(handle, 0); + + return Convert::vtf(handle)->getWidthWithoutPadding(mip); +} + SOURCEPP_API uint16_t vtfpp_vtf_get_height(vtfpp_vtf_handle_t handle, uint8_t mip) { SOURCEPP_EARLY_RETURN_VAL(handle, 0); return Convert::vtf(handle)->getHeight(mip); } +SOURCEPP_API uint16_t vtfpp_vtf_get_height_without_padding(vtfpp_vtf_handle_t handle, uint8_t mip) { + SOURCEPP_EARLY_RETURN_VAL(handle, 0); + + return Convert::vtf(handle)->getHeightWithoutPadding(mip); +} + SOURCEPP_API void vtfpp_vtf_set_size(vtfpp_vtf_handle_t handle, uint16_t width, uint16_t height, vtfpp_image_conversion_resize_filter_e filter) { SOURCEPP_EARLY_RETURN(handle); diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h index 76951d749..4d809de14 100644 --- a/lang/python/src/vtfpp.h +++ b/lang/python/src/vtfpp.h @@ -549,8 +549,12 @@ inline void register_python(py::module_& m) { .def_prop_rw("image_height_resize_method", &VTF::getImageHeightResizeMethod, &VTF::setImageHeightResizeMethod) .def_prop_ro("width", [](const VTF& self) { return self.getWidth(); }) .def("width_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getWidth(mip); }, "mip"_a = 0) + .def_prop_ro("width_without_padding", [](const VTF& self) { return self.getWidthWithoutPadding(); }) + .def("width_without_padding_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getWidthWithoutPadding(mip); }, "mip"_a = 0) .def_prop_ro("height", [](const VTF& self) { return self.getHeight(); }) .def("height_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getHeight(mip); }, "mip"_a = 0) + .def_prop_ro("height_without_padding", [](const VTF& self) { return self.getHeightWithoutPadding(); }) + .def("height_without_padding_for_mip", [](const VTF& self, uint8_t mip = 0) { return self.getHeightWithoutPadding(mip); }, "mip"_a = 0) .def("set_size", &VTF::setSize, "width"_a, "height"_a, "filter"_a) .def_prop_rw("flags", &VTF::getFlags, &VTF::setFlags) .def("add_flags", &VTF::addFlags, "flags"_a) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index c0ef216fc..06c1762f3 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -986,10 +986,18 @@ uint16_t VTF::getWidth(uint8_t mip) const { return ImageDimensions::getMipDim(mip, ImageFormatDetails::compressed(this->format), this->width); } +uint16_t VTF::getWidthWithoutPadding(uint8_t mip) const { + return ImageDimensions::getMipDim(mip, false, this->width); +} + uint16_t VTF::getHeight(uint8_t mip) const { return ImageDimensions::getMipDim(mip, ImageFormatDetails::compressed(this->format), this->height); } +uint16_t VTF::getHeightWithoutPadding(uint8_t mip) const { + return ImageDimensions::getMipDim(mip, false, this->height); +} + void VTF::setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter) { if (newWidth == 0 || newHeight == 0) { this->format = ImageFormat::EMPTY; From be07953bbe5bba110f1984e923a49b7535dc8ec5 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Tue, 28 Oct 2025 23:14:43 -0700 Subject: [PATCH 05/17] vtfpp: minor fix for the fix for pc -> xbox conversion --- src/vtfpp/VTF.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index 06c1762f3..a677eede8 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -867,10 +867,11 @@ void VTF::setPlatform(Platform newPlatform) { case PLATFORM_PC: break; case PLATFORM_XBOX: - // Have to do it this roundabout way to fix cubemaps - this->setVersion(0); + // Have to do it this roundabout way to fix cubemaps, v7.5 has 6 faces + this->setVersion(5); this->platform = newPlatform; - this->setVersion(2); + // It's safe to do this, and we can't use VTF::setVersion now that platform is set + this->version = 2; break; case PLATFORM_X360: case PLATFORM_PS3_ORANGEBOX: From 3dc2c552b0a84d515bb3e17587032c6c0c5dd09c Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sat, 1 Nov 2025 14:30:43 -0700 Subject: [PATCH 06/17] vtfpp: minor code cleanup --- src/vtfpp/HOT.cpp | 8 ++-- src/vtfpp/ImageConversion.cpp | 71 +++++++++++++++++++---------------- src/vtfpp/ImageQuantize.cpp | 2 +- src/vtfpp/SHT.cpp | 8 ++-- src/vtfpp/TTX.cpp | 8 ++-- src/vtfpp/VTF.cpp | 30 ++++++++------- 6 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/vtfpp/HOT.cpp b/src/vtfpp/HOT.cpp index 80bba1abb..7671995aa 100644 --- a/src/vtfpp/HOT.cpp +++ b/src/vtfpp/HOT.cpp @@ -17,8 +17,8 @@ HOT::HOT(std::span hotData) { } this->rects.resize(stream.read()); - for (auto& rect : this->rects) { - stream >> rect.flags >> rect.x1 >> rect.y1 >> rect.x2 >> rect.y2; + for (auto& [flags_, x1, y1, x2, y2] : this->rects) { + stream >> flags_ >> x1 >> y1 >> x2 >> y2; } this->opened = true; @@ -69,8 +69,8 @@ std::vector HOT::bake() const { stream << this->version << this->flags; stream.write(this->rects.size()); - for (const auto& rect : this->rects) { - stream << rect.flags << rect.x1 << rect.y1 << rect.x2 << rect.y2; + for (const auto& [flags_, x1, y1, x2, y2] : this->rects) { + stream << flags_ << x1 << y1 << x2 << y2; } hotspotData.resize(stream.tell()); diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index 95a65f7c6..f84b4f5b1 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -1,3 +1,8 @@ +// ReSharper disable CppDFATimeOver +// ReSharper disable CppRedundantParentheses +// ReSharper disable CppRedundantQualifier +// ReSharper disable CppUseRangeAlgorithm + #include #include @@ -972,7 +977,7 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: resolution = height; } - std::span imageDataRGBA32323232F{reinterpret_cast(imageData.data()), reinterpret_cast(imageData.data() + imageData.size())}; + std::span imageDataRGBA32323232F{reinterpret_cast(imageData.data()), reinterpret_cast(imageData.data() + imageData.size())}; std::vector possiblyConvertedDataOrEmptyDontUseMeDirectly; if (format != ImageFormat::RGBA32323232F) { @@ -1003,7 +1008,7 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: const auto up = startRightUp[i][2]; faceData[i].resize(resolution * resolution * sizeof(ImagePixel::RGBA32323232F)); - std::span face{reinterpret_cast(faceData[i].data()), reinterpret_cast(faceData[i].data() + faceData[i].size())}; + std::span face{reinterpret_cast(faceData[i].data()), reinterpret_cast(faceData[i].data() + faceData[i].size())}; for (int row = 0; row < resolution; row++) { for (int col = 0; col < resolution; col++) { @@ -1012,13 +1017,13 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: start[1] + (static_cast(col) * 2.f + 0.5f) / static_cast(resolution) * right[1] + (static_cast(row) * 2.f + 0.5f) / static_cast(resolution) * up[1], start[2] + (static_cast(col) * 2.f + 0.5f) / static_cast(resolution) * right[2] + (static_cast(row) * 2.f + 0.5f) / static_cast(resolution) * up[2], }; - float azimuth = std::atan2(pixelDirection3d[0], -pixelDirection3d[2]) + math::pi_f32; // add pi to move range to 0-360 deg - float elevation = std::atan(pixelDirection3d[1] / std::sqrt(pixelDirection3d[0] * pixelDirection3d[0] + pixelDirection3d[2] * pixelDirection3d[2])) + math::pi_f32 / 2.f; - float colHdri = (azimuth / math::pi_f32 / 2.f) * static_cast(width); // add pi to azimuth to move range to 0-360 deg - float rowHdri = (elevation / math::pi_f32) * static_cast(height); + const float azimuth = std::atan2(pixelDirection3d[0], -pixelDirection3d[2]) + math::pi_f32; // add pi to move range to 0-360 deg + const float elevation = std::atan(pixelDirection3d[1] / std::sqrt(pixelDirection3d[0] * pixelDirection3d[0] + pixelDirection3d[2] * pixelDirection3d[2])) + math::pi_f32 / 2.f; + const float colHdri = (azimuth / math::pi_f32 / 2.f) * static_cast(width); // add pi to azimuth to move range to 0-360 deg + const float rowHdri = (elevation / math::pi_f32) * static_cast(height); if (!bilinear) { - int colNearest = std::clamp(static_cast(colHdri), 0, width - 1); - int rowNearest = std::clamp(static_cast(rowHdri), 0, height - 1); + const int colNearest = std::clamp(static_cast(colHdri), 0, width - 1); + const int rowNearest = std::clamp(static_cast(rowHdri), 0, height - 1); face[col * 4 + resolution * row * 4 + 0] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 0]; face[col * 4 + resolution * row * 4 + 1] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 1]; face[col * 4 + resolution * row * 4 + 2] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 2]; @@ -1028,8 +1033,8 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: // factor gives the contribution of the next column, while the contribution of intCol is 1 - factor float factorCol = std::modf(colHdri - 0.5f, &intCol); float factorRow = std::modf(rowHdri - 0.5f, &intRow); - int low_idx_row = static_cast(intRow); - int low_idx_column = static_cast(intCol); + const int low_idx_row = static_cast(intRow); + const int low_idx_column = static_cast(intCol); int high_idx_column; if (factorCol < 0.f) { // modf can only give a negative value if the azimuth falls in the first pixel, left of the @@ -1050,10 +1055,10 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: } factorCol = std::abs(factorCol); factorRow = std::abs(factorRow); - float f1 = (1 - factorRow) * (1 - factorCol); - float f2 = factorRow * (1 - factorCol); - float f3 = (1 - factorRow) * factorCol; - float f4 = factorRow * factorCol; + const float f1 = (1 - factorRow) * (1 - factorCol); + const float f2 = factorRow * (1 - factorCol); + const float f3 = (1 - factorRow) * factorCol; + const float f4 = factorRow * factorCol; for (int j = 0; j < 4; j++) { face[col * 4 + resolution * row * 4 + j] = imageDataRGBA32323232F[low_idx_column * 4 + width * low_idx_row * 4 + j] * f1 + @@ -1070,7 +1075,7 @@ std::array, 6> ImageConversion::convertHDRIToCubeMap(std: } #ifdef SOURCEPP_BUILD_WITH_THREADS ; - std::array, 6> faceFutures{ + std::array faceFutures{ std::async(std::launch::async, faceExtraction, 0), std::async(std::launch::async, faceExtraction, 1), std::async(std::launch::async, faceExtraction, 2), @@ -1509,6 +1514,7 @@ std::vector ImageConversion::convertFileToImageData(std::span { const auto channelCount = hasRed + hasGreen + hasBlue + hasAlpha; + // ReSharper disable once CppRedundantCastExpression std::span out{reinterpret_cast(combinedChannels.data()), combinedChannels.size() / sizeof(C)}; if (header.tiled) { for (int t = 0; t < image.num_tiles; t++) { @@ -1640,12 +1646,12 @@ std::vector ImageConversion::convertFileToImageData(std::span(dir->num_frames); - static constexpr auto calcPixelOffset = [](uint32_t offsetX, uint32_t offsetY, uint32_t width) { - return ((offsetY * width) + offsetX) * sizeof(P); + static constexpr auto calcPixelOffset = [](uint32_t offsetX, uint32_t offsetY, uint32_t width_) { + return ((offsetY * width_) + offsetX) * sizeof(P); }; // Where dst is a full frame and src is a subregion - static constexpr auto copyImageData = [](std::span dst, uint32_t dstWidth, uint32_t dstHeight, std::span src, uint32_t srcWidth, uint32_t srcHeight, uint32_t srcOffsetX, uint32_t srcOffsetY) { + static constexpr auto copyImageData = [](std::span dst, uint32_t dstWidth, uint32_t /*dstHeight*/, std::span src, uint32_t srcWidth, uint32_t srcHeight, uint32_t srcOffsetX, uint32_t srcOffsetY) { for (uint32_t y = 0; y < srcHeight; y++) { std::copy( #ifdef SOURCEPP_BUILD_WITH_TBB @@ -1658,7 +1664,7 @@ std::vector ImageConversion::convertFileToImageData(std::span dst, std::span src, uint32_t imgWidth, uint32_t imgHeight, uint32_t subWidth, uint32_t subHeight, uint32_t subOffsetX, uint32_t subOffsetY) { + static constexpr auto copyImageSubRectData = [](std::span dst, std::span src, uint32_t imgWidth, uint32_t /*imgHeight*/, uint32_t subWidth, uint32_t subHeight, uint32_t subOffsetX, uint32_t subOffsetY) { for (uint32_t y = subOffsetY; y < subOffsetY + subHeight; y++) { std::copy( #ifdef SOURCEPP_BUILD_WITH_TBB @@ -1670,7 +1676,7 @@ std::vector ImageConversion::convertFileToImageData(std::span dst, uint32_t dstWidth, uint32_t dstHeight, uint32_t clrWidth, uint32_t clrHeight, uint32_t clrOffsetX, uint32_t clrOffsetY) { + static constexpr auto clearImageData = [](std::span dst, uint32_t dstWidth, uint32_t /*dstHeight*/, uint32_t clrWidth, uint32_t clrHeight, uint32_t clrOffsetX, uint32_t clrOffsetY) { for (uint32_t y = 0; y < clrHeight; y++) { std::transform( #ifdef SOURCEPP_BUILD_WITH_TBB @@ -1683,19 +1689,20 @@ std::vector ImageConversion::convertFileToImageData(std::span dst, uint32_t dstWidth, uint32_t dstHeight, std::span src, uint32_t srcWidth, uint32_t srcHeight, uint32_t srcOffsetX, uint32_t srcOffsetY) { + static constexpr auto overlayImageData = [](std::span dst, uint32_t dstWidth, uint32_t /*dstHeight*/, std::span src, uint32_t srcWidth, uint32_t srcHeight, uint32_t srcOffsetX, uint32_t srcOffsetY) { for (uint32_t y = 0; y < srcHeight; y++) { const auto* sp = reinterpret_cast(src.data() + calcPixelOffset(0, y, srcWidth)); auto* dp = reinterpret_cast(dst.data() + calcPixelOffset(srcOffsetX, srcOffsetY + y, dstWidth)); for (uint32_t x = 0; x < srcWidth; x++, sp += 4, dp += 4) { if (sp[3] == 0) { continue; - } else if ((sp[3] == 0xff) || (dp[3] == 0)) { - std::copy(sp, sp + sizeof(P), dp); + } + if ((sp[3] == 0xff) || (dp[3] == 0)) { + std::copy_n(sp, sizeof(P), dp); } else { - int u = sp[3] * 0xff; - int v = (0xff - sp[3]) * dp[3]; - int al = u + v; + const int u = sp[3] * 0xff; + const int v = (0xff - sp[3]) * dp[3]; + const int al = u + v; dp[0] = (sp[0] * u + dp[0] * v) / al; dp[1] = (sp[1] * u + dp[1] * v) / al; dp[2] = (sp[2] * u + dp[2] * v) / al; @@ -1794,7 +1801,7 @@ std::vector ImageConversion::convertFileToImageData(std::span(stbImage, dirOffset); + return apngDecoder.operator()(stbImage, dirOffset); } } else { const ::stb_ptr stbImage{ @@ -1802,7 +1809,7 @@ std::vector ImageConversion::convertFileToImageData(std::span(stbImage, dirOffset); + return apngDecoder.operator()(stbImage, dirOffset); } } } @@ -1846,10 +1853,10 @@ std::vector ImageConversion::convertFileToImageData(std::span out(ImageFormatDetails::getDataLength(format, width, height)); - std::span outPixels{reinterpret_cast(out.data()), out.size() / sizeof(ImagePixel::RGBA16161616)}; + std::span outPixels{reinterpret_cast(out.data()), out.size() / sizeof(ImagePixel::RGBA16161616)}; switch (channels) { case 1: { - std::span inPixels{reinterpret_cast(stbImage.get()), outPixels.size()}; + std::span inPixels{stbImage.get(), outPixels.size()}; std::transform( #ifdef SOURCEPP_BUILD_WITH_TBB std::execution::par_unseq, @@ -1864,7 +1871,7 @@ std::vector ImageConversion::convertFileToImageData(std::span inPixels{reinterpret_cast(stbImage.get()), outPixels.size()}; + std::span inPixels{reinterpret_cast(stbImage.get()), outPixels.size()}; std::transform( #ifdef SOURCEPP_BUILD_WITH_TBB std::execution::par_unseq, @@ -1880,7 +1887,7 @@ std::vector ImageConversion::convertFileToImageData(std::span inPixels{reinterpret_cast(stbImage.get()), outPixels.size()}; + std::span inPixels{reinterpret_cast(stbImage.get()), outPixels.size()}; std::transform( #ifdef SOURCEPP_BUILD_WITH_TBB std::execution::par_unseq, diff --git a/src/vtfpp/ImageQuantize.cpp b/src/vtfpp/ImageQuantize.cpp index ebe2fdf83..7a3efeed9 100644 --- a/src/vtfpp/ImageQuantize.cpp +++ b/src/vtfpp/ImageQuantize.cpp @@ -7,7 +7,7 @@ std::vector ImageQuantize::convertP8ImageDataToBGRA8888(std::span(paletteData.data()), 256}; + const std::span palettePixelData{reinterpret_cast(paletteData.data()), 256}; std::vector out; out.resize(imageData.size() * sizeof(ImagePixel::BGRA8888)); diff --git a/src/vtfpp/SHT.cpp b/src/vtfpp/SHT.cpp index 18ee40eff..32f1174aa 100644 --- a/src/vtfpp/SHT.cpp +++ b/src/vtfpp/SHT.cpp @@ -1,3 +1,5 @@ +// ReSharper disable CppUseStructuredBinding + #include #include @@ -58,7 +60,7 @@ std::vector& SHT::getSequences() { } const SHT::Sequence* SHT::getSequenceFromID(uint32_t id) const { - if (auto pos = std::find_if(this->sequences.begin(), this->sequences.end(), [id](const Sequence& sequence) { + if (const auto pos = std::ranges::find_if(this->sequences, [id](const Sequence& sequence) { return sequence.id == id; }); pos != this->sequences.end()) { return &*pos; @@ -67,7 +69,7 @@ const SHT::Sequence* SHT::getSequenceFromID(uint32_t id) const { } SHT::Sequence* SHT::getSequenceFromID(uint32_t id) { - if (auto pos = std::find_if(this->sequences.begin(), this->sequences.end(), [id](const Sequence& sequence) { + if (const auto pos = std::ranges::find_if(this->sequences, [id](const Sequence& sequence) { return sequence.id == id; }); pos != this->sequences.end()) { return &*pos; @@ -76,7 +78,7 @@ SHT::Sequence* SHT::getSequenceFromID(uint32_t id) { } uint8_t SHT::getFrameBoundsCount() const { - return (this->version > 0) ? 4 : 1; + return this->version > 0 ? 4 : 1; } std::vector SHT::bake() const { diff --git a/src/vtfpp/TTX.cpp b/src/vtfpp/TTX.cpp index 2acf24768..0ea4b384e 100644 --- a/src/vtfpp/TTX.cpp +++ b/src/vtfpp/TTX.cpp @@ -169,10 +169,10 @@ std::pair, std::vector> TTX::bake() const { } bool TTX::bake(const std::string& tthPath, const std::string& ttzPath) const { - const auto data = this->bake(); - const bool tth = fs::writeFileBuffer(tthPath, data.first); - if (!data.second.empty()) { - return fs::writeFileBuffer(ttzPath, data.second) && tth; + const auto [tthData, ttzData] = this->bake(); + const bool tth = fs::writeFileBuffer(tthPath, tthData); + if (!ttzData.empty()) { + return fs::writeFileBuffer(ttzPath, ttzData) && tth; } return tth; } diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index a677eede8..358d416eb 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -1,3 +1,8 @@ +// ReSharper disable CppDFATimeOver +// ReSharper disable CppRedundantParentheses +// ReSharper disable CppRedundantQualifier +// ReSharper disable CppUseStructuredBinding + #include #include @@ -466,7 +471,7 @@ VTF::VTF(std::vector&& vtfData, bool parseHeaderOnly) } break; case ZSTD: - if (auto decompressedSize = ZSTD_decompress(reinterpret_cast(decompressedImageData.data() + newOffset), decompressedImageDataSize, reinterpret_cast(imageResource->data.data() + oldOffset), oldLength); ZSTD_isError(decompressedSize) || decompressedSize != decompressedImageDataSize) { + if (const auto decompressedSize = ZSTD_decompress(decompressedImageData.data() + newOffset, decompressedImageDataSize, imageResource->data.data() + oldOffset, oldLength); ZSTD_isError(decompressedSize) || decompressedSize != decompressedImageDataSize) { this->opened = false; return; } @@ -767,9 +772,9 @@ bool VTF::createInternal(VTF& writer, CreationOptions options) { if (options.computeThumbnail) { writer.computeThumbnail(); } - if (options.outputFormat == VTF::FORMAT_UNCHANGED) { + if (options.outputFormat == FORMAT_UNCHANGED) { options.outputFormat = writer.getFormat(); - } else if (options.outputFormat == VTF::FORMAT_DEFAULT) { + } else if (options.outputFormat == FORMAT_DEFAULT) { options.outputFormat = VTF::getDefaultCompressedFormat(writer.getFormat(), writer.getVersion(), options.isCubeMap); } if (options.computeMips) { @@ -1054,13 +1059,13 @@ void VTF::setSRGB(bool srgb) { if (srgb) { if (this->version >= 5) { this->addFlags(FLAG_V5_SRGB); - } else if (this->version >= 4) { + } else if (this->version == 4) { this->addFlags(FLAG_V4_SRGB); } } else { if (this->version >= 5) { this->removeFlags(FLAG_V5_PWL_CORRECTED | FLAG_V5_SRGB); - } else if (this->version >= 4) { + } else if (this->version == 4) { this->removeFlags(FLAG_V4_SRGB); } } @@ -1076,8 +1081,7 @@ void VTF::computeTransparencyFlags() { this->flags &= ~FLAG_V0_MULTI_BIT_ALPHA; } } else { - this->flags &= ~FLAG_V0_ONE_BIT_ALPHA; - this->flags &= ~FLAG_V0_MULTI_BIT_ALPHA; + this->flags &= ~(FLAG_V0_ONE_BIT_ALPHA | FLAG_V0_MULTI_BIT_ALPHA); } } @@ -1099,10 +1103,10 @@ ImageFormat VTF::getFormat() const { } void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter, float quality) { - if (newFormat == VTF::FORMAT_UNCHANGED || newFormat == this->format) { + if (newFormat == FORMAT_UNCHANGED || newFormat == this->format) { return; } - if (newFormat == VTF::FORMAT_DEFAULT) { + if (newFormat == FORMAT_DEFAULT) { newFormat = VTF::getDefaultCompressedFormat(this->format, this->version, this->getFaceCount() > 1); } if (!this->hasImageData()) { @@ -1235,11 +1239,11 @@ uint8_t VTF::getFaceCount() const { return 1; } -bool VTF::setFaceCount(bool isCubemap) { +bool VTF::setFaceCount(bool isCubeMap) { if (!this->hasImageData()) { return false; } - this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, isCubemap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, this->depth); + this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, isCubeMap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, this->depth); return true; } @@ -1275,7 +1279,7 @@ math::Vec3f VTF::getReflectivity() const { return this->reflectivity; } -void VTF::setReflectivity(sourcepp::math::Vec3f newReflectivity) { +void VTF::setReflectivity(math::Vec3f newReflectivity) { this->reflectivity = newReflectivity; } @@ -1551,7 +1555,7 @@ std::vector VTF::getParticleSheetFrameDataRaw(uint16_t& spriteWidth, spriteWidth = 0; spriteHeight = 0; - auto shtResource = this->getResource(Resource::TYPE_PARTICLE_SHEET_DATA); + const auto shtResource = this->getResource(Resource::TYPE_PARTICLE_SHEET_DATA); if (!shtResource) { return {}; } From f3d88551244eea125b3fc4ba5eecf7eb5425a56d Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sat, 1 Nov 2025 15:01:13 -0700 Subject: [PATCH 07/17] kvpp: add creation from string literal operators --- include/kvpp/KV1.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/kvpp/KV1.h b/include/kvpp/KV1.h index ffdd66cdf..d5547e2c8 100644 --- a/include/kvpp/KV1.h +++ b/include/kvpp/KV1.h @@ -431,4 +431,16 @@ class KV1Writer : public KV1ElementWritable { bool useEscapeSequences; }; +namespace literals { + +inline KV1<> operator""_kv1(const char* str, const std::size_t len) { + return KV1{std::string_view{str, len}}; +} + +inline KV1Writer<> operator""_kv1w(const char* str, const std::size_t len) { + return KV1Writer{std::string_view{str, len}}; +} + +} // namespace literals + } // namespace kvpp From 68d387eee502171bcafac5c00b26576955798458 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sat, 1 Nov 2025 19:02:49 -0700 Subject: [PATCH 08/17] cmake: update homepage url --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bcaa83bba..9d4fb0529 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.28 FATAL_ERROR) # Create project project(sourcepp DESCRIPTION "Several modern C++20 libraries for sanely parsing Valve formats." - HOMEPAGE_URL "https://craftablescience.info/sourcepp") + HOMEPAGE_URL "https://sourcepp.org") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) From c18bc016b4f4b833eff916c7a816704ab97ec3cb Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 10:06:42 -0800 Subject: [PATCH 09/17] bsppp: use BufferStream::pad utility method --- src/bsppp/BSP.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/bsppp/BSP.cpp b/src/bsppp/BSP.cpp index 4cc55cb76..883dee7cc 100644 --- a/src/bsppp/BSP.cpp +++ b/src/bsppp/BSP.cpp @@ -390,9 +390,7 @@ bool BSP::bake(std::string_view outputPath) { // Lumps are 4 byte aligned if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) { - for (int p = 0; p < padding; p++) { - stream.write(0); - } + stream.pad(padding); } if (static_cast(i) == BSPLump::GAME_LUMP && !this->stagedGameLumps.empty()) { @@ -507,9 +505,7 @@ bool BSP::bake(std::string_view outputPath) { // Lumps are 4 byte aligned if (const auto padding = math::paddingForAlignment(4, stream.tell()); padding > 0) { - for (int p = 0; p < padding; p++) { - stream.write(0); - } + stream.pad(padding); } out.resize(stream.size()); From 1faece4abe87f2c85cfb3cde1e555a04d031990a Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 10:19:41 -0800 Subject: [PATCH 10/17] sndpp: update future plans --- FUTURE.md | 2 ++ include/sndpp/XWV.h | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/FUTURE.md b/FUTURE.md index 20797c754..f725cf889 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -30,8 +30,10 @@ - Make something to construct StudioModel objects from a given model file like obj/glTF? - Parse animations/sequences - `sndpp` + - HL2X XWV read/write support - WAV write support - XWV write support + - Conversion support between all formats - `toolpp` - Perhaps add the ability to parse TeamSpen's additions to the FGD format? - `vpkpp` diff --git a/include/sndpp/XWV.h b/include/sndpp/XWV.h index 0bff038cf..a65f5a13c 100644 --- a/include/sndpp/XWV.h +++ b/include/sndpp/XWV.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include From 7de2b2a85afa4854dabbb426abe2715494c85c09 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:03:41 -0800 Subject: [PATCH 11/17] vtfpp: fix VTF::computeThumbnail for XBOX platform --- src/vtfpp/VTF.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index 358d416eb..78a4665ba 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -896,15 +896,7 @@ void VTF::setPlatform(Platform newPlatform) { // 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); + this->computeThumbnail(); } else if (oldPlatform == PLATFORM_XBOX) { this->thumbnailFormat = ImageFormat::EMPTY; this->thumbnailWidth = 0; @@ -1872,10 +1864,23 @@ void VTF::computeThumbnail(ImageConversion::ResizeFilter filter, float quality) if (!this->hasImageData()) { return; } - this->thumbnailFormat = ImageFormat::DXT1; - this->thumbnailWidth = 16; - this->thumbnailHeight = 16; - this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, ImageConversion::convertImageDataToFormat(ImageConversion::resizeImageData(this->getImageDataRaw(), this->format, this->width, this->thumbnailWidth, this->height, this->thumbnailHeight, this->isSRGB(), filter), this->format, this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight, quality)); + + 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 { + this->thumbnailFormat = ImageFormat::DXT1; + this->thumbnailWidth = 16; + this->thumbnailHeight = 16; + this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, ImageConversion::convertImageDataToFormat(ImageConversion::resizeImageData(this->getImageDataRaw(), this->format, this->width, this->thumbnailWidth, this->height, this->thumbnailHeight, this->isSRGB(), filter), this->format, this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight, quality)); + } } void VTF::removeThumbnail() { From ba9f5efe636787888e3c7610f6d140b11401f862 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:04:48 -0800 Subject: [PATCH 12/17] vtfpp: fix VTF::setFormat and VTF::setImage trying to resize dimensions to a power of 4 given a compressed format --- src/vtfpp/VTF.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index 78a4665ba..d6a1b36f9 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -1106,11 +1106,7 @@ void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter, return; } const auto oldFormat = this->format; - if (ImageFormatDetails::compressed(newFormat)) { - this->regenerateImageData(newFormat, this->width + math::paddingForAlignment(4, this->width), this->height + math::paddingForAlignment(4, this->height), this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); - } else { - this->regenerateImageData(newFormat, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); - } + this->regenerateImageData(newFormat, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality); if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA)) { const auto fallbackConverted = ImageConversion::convertSeveralImageDataToFormat(fallbackResource->data, oldFormat, this->format, ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight), this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, quality); @@ -1728,10 +1724,6 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u if (!this->hasImageData()) { uint16_t resizedWidth = width_, resizedHeight = height_; ImageConversion::setResizedDims(resizedWidth, this->imageWidthResizeMethod, resizedHeight, this->imageHeightResizeMethod); - if (ImageFormatDetails::compressed(format_)) { - resizedWidth += math::paddingForAlignment(4, resizedWidth); - resizedHeight += math::paddingForAlignment(4, resizedHeight); - } if (const auto newMipCount = ImageDimensions::getMaximumMipCount(resizedWidth, resizedHeight, this->depth); newMipCount <= mip) { mip = newMipCount - 1; } From e3aa089a9501a94ed5c687ed86ceee2ba3632c9d Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:05:25 -0800 Subject: [PATCH 13/17] vtfpp: cubemaps have 6 faces, not 5, fortunately this codepath seems to have never been hit yet --- src/vtfpp/VTF.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index d6a1b36f9..faa9f43b4 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -1730,7 +1730,7 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u if (face > 6 || (face == 6 && (this->version < 1 || this->version > 4))) { return false; } - this->regenerateImageData(format_, resizedWidth, resizedHeight, mip + 1, frame + 1, face ? (face < 5 ? 5 : face) : 0, slice + 1); + this->regenerateImageData(format_, resizedWidth, resizedHeight, mip + 1, frame + 1, face ? (face < 6 ? 6 : face) : 0, slice + 1); } const auto faceCount = this->getFaceCount(); From 37f7b0c5f789bfb140ac66fbc94de91c7907fd06 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:05:56 -0800 Subject: [PATCH 14/17] vtfpp: fix hopefully last bug where XBOX platform was assumed to have 7 faces --- src/vtfpp/VTF.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vtfpp/VTF.cpp b/src/vtfpp/VTF.cpp index faa9f43b4..413c9b124 100644 --- a/src/vtfpp/VTF.cpp +++ b/src/vtfpp/VTF.cpp @@ -1727,7 +1727,7 @@ bool VTF::setImage(std::span imageData_, ImageFormat format_, u if (const auto newMipCount = ImageDimensions::getMaximumMipCount(resizedWidth, resizedHeight, this->depth); newMipCount <= mip) { mip = newMipCount - 1; } - if (face > 6 || (face == 6 && (this->version < 1 || this->version > 4))) { + if (face > 6 || (face == 6 && (this->platform == PLATFORM_XBOX || this->version < 1 || this->version > 4))) { return false; } this->regenerateImageData(format_, resizedWidth, resizedHeight, mip + 1, frame + 1, face ? (face < 6 ? 6 : face) : 0, slice + 1); From 1360775cf1f9d89572da89604a07737d1414adcc Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:44:27 -0800 Subject: [PATCH 15/17] vtfpp: create ImageConversion::padImageData --- include/vtfpp/ImageConversion.h | 3 +++ lang/python/src/vtfpp.h | 5 +++++ src/vtfpp/ImageConversion.cpp | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/include/vtfpp/ImageConversion.h b/include/vtfpp/ImageConversion.h index 419cfde88..49e6f1bae 100644 --- a/include/vtfpp/ImageConversion.h +++ b/include/vtfpp/ImageConversion.h @@ -413,6 +413,9 @@ void setResizedDims(uint16_t& width, ResizeMethod widthResize, uint16_t& height, /// Crops the given image to the new dimensions. If the image format is compressed it will be converted to its container format before the crop, and converted back before returning [[nodiscard]] std::vector cropImageData(std::span imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t xOffset, uint16_t height, uint16_t newHeight, uint16_t yOffset); +/// Pad the given image with pixels that are the same color as the edge. Padding is applied to the right and bottom of the image. +[[nodiscard]] std::vector padImageData(std::span imageData, ImageFormat format, uint16_t width, uint16_t widthPad, uint16_t height, uint16_t heightPad); + /// Perform gamma correction on the given image data. Will not perform gamma correction if the input image format is large, console, P8, A8, UV88, UVLX8888, or UVWQ8888 [[nodiscard]] std::vector gammaCorrectImageData(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, float gamma); diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h index 4d809de14..ac02bf081 100644 --- a/lang/python/src/vtfpp.h +++ b/lang/python/src/vtfpp.h @@ -246,6 +246,11 @@ inline void register_python(py::module_& m) { return py::bytes{d.data(), d.size()}; }, "image_data"_a, "format"_a, "width"_a, "new_width"_a, "x_offset"_a, "height"_a, "new_height"_a, "y_offset"_a); + ImageConversion.def("pad_image_data", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t widthPad, uint16_t height, uint16_t heightPad) { + const auto d = padImageData({static_cast(imageData.data()), imageData.size()}, format, width, widthPad, height, heightPad); + return py::bytes{d.data(), d.size()}; + }, "image_data"_a, "format"_a, "width"_a, "width_pad"_a, "height"_a, "height_pad"_a); + ImageConversion.def("gamma_correct_image_data", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, float gamma) { const auto d = gammaCorrectImageData({static_cast(imageData.data()), imageData.size()}, format, width, height, gamma); return py::bytes{d.data(), d.size()}; diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index f84b4f5b1..ea4353270 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -2049,6 +2049,43 @@ std::vector ImageConversion::cropImageData(std::span return out; } +// NOLINTNEXTLINE(*-no-recursion) +std::vector ImageConversion::padImageData(std::span imageData, ImageFormat format, uint16_t width, uint16_t widthPad, uint16_t height, uint16_t heightPad) { + if (imageData.empty() || format == ImageFormat::EMPTY || !width || !height) { + return {}; + } + if (!widthPad && !heightPad) { + return {imageData.begin(), imageData.end()}; + } + if (ImageFormatDetails::compressed(format)) { + // This is horrible but what can you do? + const auto container = ImageFormatDetails::containerFormat(format); + return convertImageDataToFormat(padImageData(convertImageDataToFormat(imageData, format, container, width, height), container, width, widthPad, height, heightPad), container, format, width + widthPad, height + heightPad); + } + + const auto pixelSize = ImageFormatDetails::bpp(format) / 8; + std::vector out(pixelSize * (width + widthPad) * (height * heightPad)); + + // Copy existing image in + for (uint16_t y = 0; y < height; y++) { + std::memcpy(out.data() + ((y * (width + widthPad)) * pixelSize), imageData.data() + ((y * width) * pixelSize), width * pixelSize); + } + + // Color padding + for (int y = 0; y < height + heightPad; y++) { + for (int x = 0; x < width + widthPad; x++) { + if (x >= width && y >= height) { + std::memcpy(out.data() + ((y * (width + widthPad) + x) * pixelSize), imageData.data() + (((height - 1) * (width) + (width - 1)) * pixelSize), pixelSize); + } else if (x >= width) { + std::memcpy(out.data() + ((y * (width + widthPad) + x) * pixelSize), imageData.data() + ((y * (width) + (width - 1)) * pixelSize), pixelSize); + } else if (y >= height) { + std::memcpy(out.data() + ((y * (width + widthPad) + x) * pixelSize), imageData.data() + (((height - 1) * (width) + x) * pixelSize), pixelSize); + } + } + } + return out; +} + // NOLINTNEXTLINE(*-no-recursion) std::vector ImageConversion::gammaCorrectImageData(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, float gamma) { if (imageData.empty() || format == ImageFormat::EMPTY) { From 813624212eccb53e596985ea396cded70620b7e3 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 15:50:24 -0800 Subject: [PATCH 16/17] vtfpp: pad image data to multiple of 4 dimensions if compressing --- src/vtfpp/ImageConversion.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index ea4353270..ed1f69325 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -299,6 +299,14 @@ namespace { return {}; } + std::vector paddedImageData; + if (!ImageFormatDetails::compressed(oldFormat) && ImageFormatDetails::compressed(newFormat) && (width % 4 != 0 || height % 4 != 0)) { + paddedImageData = ImageConversion::padImageData(imageData, oldFormat, width, width % 4, height, height % 4); + imageData = paddedImageData; + width += width % 4; + height += height % 4; + } + CMP_Texture srcTexture{}; srcTexture.dwSize = sizeof(srcTexture); srcTexture.dwWidth = width; From 627939afdb8d710a6e6caef3d52e557d7ab3bd27 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 2 Nov 2025 16:26:06 -0800 Subject: [PATCH 17/17] vtfpp: crop image data if it has padding after decompression (also fix pad function allocating way too much space in return buffer) --- src/vtfpp/ImageConversion.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index ed1f69325..3dc9fefff 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -299,12 +299,14 @@ namespace { return {}; } + uint16_t unpaddedWidth = width, unpaddedHeight = height; std::vector paddedImageData; - if (!ImageFormatDetails::compressed(oldFormat) && ImageFormatDetails::compressed(newFormat) && (width % 4 != 0 || height % 4 != 0)) { - paddedImageData = ImageConversion::padImageData(imageData, oldFormat, width, width % 4, height, height % 4); + if ((width % 4 != 0 || height % 4 != 0) && !ImageFormatDetails::compressed(oldFormat) && ImageFormatDetails::compressed(newFormat)) { + uint16_t paddingWidth = (4 - (width % 4)) % 4, paddingHeight = (4 - (height % 4)) % 4; + paddedImageData = ImageConversion::padImageData(imageData, oldFormat, width, paddingWidth, height, paddingHeight); imageData = paddedImageData; - width += width % 4; - height += height % 4; + width += paddingWidth; + height += paddingHeight; } CMP_Texture srcTexture{}; @@ -346,6 +348,10 @@ namespace { if (CMP_ConvertTexture(&srcTexture, &destTexture, &options, nullptr) != CMP_OK) { return {}; } + + if ((unpaddedWidth % 4 != 0 || unpaddedHeight % 4 != 0) && ImageFormatDetails::compressed(oldFormat) && !ImageFormatDetails::compressed(newFormat) ) { + return ImageConversion::cropImageData(destData, newFormat, width, unpaddedWidth, 0, height, unpaddedHeight, 0); + } return destData; } @@ -2072,7 +2078,7 @@ std::vector ImageConversion::padImageData(std::span } const auto pixelSize = ImageFormatDetails::bpp(format) / 8; - std::vector out(pixelSize * (width + widthPad) * (height * heightPad)); + std::vector out(pixelSize * (width + widthPad) * (height + heightPad)); // Copy existing image in for (uint16_t y = 0; y < height; y++) {