From e47ab9d0e830f6ce4e0d61a1acf5136ef4128be7 Mon Sep 17 00:00:00 2001 From: bmdhacks Date: Tue, 26 Aug 2025 12:49:04 -0700 Subject: [PATCH 1/5] KTX ASTC Support I removed some vestiges of ASTC support in DDS, and fixed a few math bugs regarding pixel sizes and compressed size for astc. Also, we don't try to invert ASTC KTX textures becaue they're fed to opengl correctly as long as the origin is set accurately. --- src/osg/Image.cpp | 16 +++++++++------ src/osg/Texture.cpp | 28 +++++++++++++------------- src/osgPlugins/ktx/ReaderWriterKTX.cpp | 3 +++ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/osg/Image.cpp b/src/osg/Image.cpp index 9a09f4082f3..b29f4b0dafe 100644 --- a/src/osg/Image.cpp +++ b/src/osg/Image.cpp @@ -806,15 +806,15 @@ unsigned int Image::computePixelSizeInBits(GLenum format,GLenum type) { osg::Vec3i footprint = computeBlockFootprint(format); unsigned int pixelsPerBlock = footprint.x() * footprint.y(); - unsigned int bitsPerBlock = computeBlockSize(format, 0);//16 x 8 = 128 + unsigned int bitsPerBlock = computeBlockSize(format, 0) * 8; // Convert bytes to bits unsigned int bitsPerPixel = bitsPerBlock / pixelsPerBlock; if (bitsPerBlock == bitsPerPixel * pixelsPerBlock) { - OSG_WARN << "Image::computePixelSizeInBits(format,type) : bits per pixel (" << bitsPerPixel << ") is not an integer for GL_KHR_texture_compression_astc_hdr sizes other than 4x4 and 8x8." << std::endl; + // Integer division worked perfectly return bitsPerPixel; } else { OSG_WARN << "Image::computePixelSizeInBits(format,type) : bits per pixel (" << bitsPerBlock << "/" << pixelsPerBlock << ") is not an integer for GL_KHR_texture_compression_astc_hdr size" << footprint.x() << "x" << footprint.y() << "." << std::endl; + return 0; } - return 0; } default: break; } @@ -1827,6 +1827,8 @@ void Image::flipVertical() const bool dxtc(dxtc_tool::isDXTC(_pixelFormat)); const bool rgtc(dxtc_tool::isRGTC(_pixelFormat)); + const bool astc(isCompressed() && (_pixelFormat >= GL_COMPRESSED_RGBA_ASTC_4x4_KHR && _pixelFormat <= GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR)); + if (_mipmapData.empty()) { // no mipmaps, @@ -1840,9 +1842,10 @@ void Image::flipVertical() OSG_NOTICE << "Notice Image::flipVertical(): Vertical flip do not succeed" << std::endl; } } - else + // ASTC textures are stored in KTX format with native OpenGL orientation - skip flipping + else if (!astc) { - if (isCompressed()) OSG_NOTICE << "Notice Image::flipVertical(): image is compressed but normal v-flip is used" << std::endl; + if (isCompressed()) OSG_NOTICE << "Notice Image::flipVertical(): file=" << _fileName << " image is compressed but normal v-flip is used" << std::endl; // its not a compressed image, so implement flip oursleves. unsigned char* top = data(0,0,r); unsigned char* bottom = top + (_t-1)*rowStep; @@ -1887,7 +1890,8 @@ void Image::flipVertical() OSG_NOTICE << "Notice Image::flipVertical(): Vertical flip did not succeed" << std::endl; } } - else + // ASTC textures are stored in KTX format with native OpenGL orientation - skip flipping + else if (!astc) { // it's not a compressed image, so implement flip ourselves. unsigned int mipRowSize = computeRowWidthInBytes(s, _pixelFormat, _dataType, _packing); diff --git a/src/osg/Texture.cpp b/src/osg/Texture.cpp index 2c0bd4b4ebb..189d941ef81 100644 --- a/src/osg/Texture.cpp +++ b/src/osg/Texture.cpp @@ -1995,85 +1995,85 @@ void Texture::getCompressedSize(GLenum internalFormat, GLint width, GLint height else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_4x4_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR) { blockSize = 16; - size = ceil(width/4.0)*ceil(height/4.0)*blockSize; + size = ((width+3)/4)*((height+3)/4)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_5x4_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR) { blockSize = 16; - size = ceil(width/5.0)*ceil(height/4.0)*blockSize; + size = ((width+4)/5)*((height+3)/4)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_5x5_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR) { blockSize = 16; - size = ceil(width/5.0)*ceil(height/5.0)*blockSize; + size = ((width+4)/5)*((height+4)/5)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_6x5_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR) { blockSize = 16; - size = ceil(width/6.0)*ceil(height/5.0)*blockSize; + size = ((width+5)/6)*((height+4)/5)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_6x6_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR) { blockSize = 16; - size = ceil(width/6.0)*ceil(height/6.0)*blockSize; + size = ((width+5)/6)*((height+5)/6)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_8x5_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR) { blockSize = 16; - size = ceil(width/8.0)*ceil(height/5.0)*blockSize; + size = ((width+7)/8)*((height+4)/5)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_8x6_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR) { blockSize = 16; - size = ceil(width/8.0)*ceil(height/6.0)*blockSize; + size = ((width+7)/8)*((height+5)/6)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_8x8_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR) { blockSize = 16; - size = ceil(width/8.0)*ceil(height/8.0)*blockSize; + size = ((width+7)/8)*((height+7)/8)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_10x5_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR) { blockSize = 16; - size = ceil(width/10.0)*ceil(height/5.0)*blockSize; + size = ((width+9)/10)*((height+4)/5)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_10x6_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR) { blockSize = 16; - size = ceil(width/10.0)*ceil(height/6.0)*blockSize; + size = ((width+9)/10)*((height+5)/6)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_10x8_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR) { blockSize = 16; - size = ceil(width/10.0)*ceil(height/8.0)*blockSize; + size = ((width+9)/10)*((height+7)/8)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_10x10_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR) { blockSize = 16; - size = ceil(width/10.0)*ceil(height/10.0)*blockSize; + size = ((width+9)/10)*((height+9)/10)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_12x10_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR) { blockSize = 16; - size = ceil(width/12.0)*ceil(height/10.0)*blockSize; + size = ((width+11)/12)*((height+9)/10)*depth*blockSize; return; } else if (internalFormat == GL_COMPRESSED_RGBA_ASTC_12x12_KHR || internalFormat == GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR) { blockSize = 16; - size = ceil(width/12.0)*ceil(height/12.0)*blockSize; + size = ((width+11)/12)*((height+11)/12)*depth*blockSize; return; } else diff --git a/src/osgPlugins/ktx/ReaderWriterKTX.cpp b/src/osgPlugins/ktx/ReaderWriterKTX.cpp index a14d35c5761..184dc88b289 100644 --- a/src/osgPlugins/ktx/ReaderWriterKTX.cpp +++ b/src/osgPlugins/ktx/ReaderWriterKTX.cpp @@ -210,6 +210,9 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin header.glInternalFormat, header.glFormat, header.glType, totalImageData, osg::Image::USE_NEW_DELETE); + // KTX textures are stored in their original coordinate system (TOP_LEFT) + image->setOrigin(osg::Image::TOP_LEFT); + if (header.numberOfMipmapLevels > 1) image->setMipmapLevels(mipmapData); From 987984b06fc459e76991013a5584472c853ecc28 Mon Sep 17 00:00:00 2001 From: bmdhacks Date: Fri, 5 Sep 2025 08:33:37 -0700 Subject: [PATCH 2/5] Look at the ktxOrientation metadata field for ktx textures in order to ensure compatible textures. --- src/osg/Image.cpp | 7 +- src/osgPlugins/ktx/ReaderWriterKTX.cpp | 108 ++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/osg/Image.cpp b/src/osg/Image.cpp index b29f4b0dafe..bf55cbf74fe 100644 --- a/src/osg/Image.cpp +++ b/src/osg/Image.cpp @@ -1827,7 +1827,6 @@ void Image::flipVertical() const bool dxtc(dxtc_tool::isDXTC(_pixelFormat)); const bool rgtc(dxtc_tool::isRGTC(_pixelFormat)); - const bool astc(isCompressed() && (_pixelFormat >= GL_COMPRESSED_RGBA_ASTC_4x4_KHR && _pixelFormat <= GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR)); if (_mipmapData.empty()) { @@ -1842,8 +1841,7 @@ void Image::flipVertical() OSG_NOTICE << "Notice Image::flipVertical(): Vertical flip do not succeed" << std::endl; } } - // ASTC textures are stored in KTX format with native OpenGL orientation - skip flipping - else if (!astc) + else { if (isCompressed()) OSG_NOTICE << "Notice Image::flipVertical(): file=" << _fileName << " image is compressed but normal v-flip is used" << std::endl; // its not a compressed image, so implement flip oursleves. @@ -1890,8 +1888,7 @@ void Image::flipVertical() OSG_NOTICE << "Notice Image::flipVertical(): Vertical flip did not succeed" << std::endl; } } - // ASTC textures are stored in KTX format with native OpenGL orientation - skip flipping - else if (!astc) + else { // it's not a compressed image, so implement flip ourselves. unsigned int mipRowSize = computeRowWidthInBytes(s, _pixelFormat, _dataType, _packing); diff --git a/src/osgPlugins/ktx/ReaderWriterKTX.cpp b/src/osgPlugins/ktx/ReaderWriterKTX.cpp index 184dc88b289..3560c3760ba 100644 --- a/src/osgPlugins/ktx/ReaderWriterKTX.cpp +++ b/src/osgPlugins/ktx/ReaderWriterKTX.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include // Macro similar to what's in FLT/TRP plugins (except it uses wide char under Windows if OSG_USE_UTF8_FILENAME) #if defined(_WIN32) @@ -115,8 +117,96 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin if (header.numberOfMipmapLevels == 0) header.numberOfMipmapLevels = 1; - //read keyvalue data. Will be ignoring for now - fin.ignore(header.bytesOfKeyValueData); + // Parse key-value metadata to check for KTXorientation + bool hasValidOrientation = true; // Default to true for non-ASTC or when no metadata + std::string ktxOrientation; + + if (header.bytesOfKeyValueData > 0) + { + std::vector kvData(header.bytesOfKeyValueData); + fin.read(kvData.data(), header.bytesOfKeyValueData); + if (!fin.good()) + { + OSG_WARN << "Failed to read KTX key-value data." << std::endl; + return ReadResult(ReadResult::ERROR_IN_READING_FILE); + } + + // Parse key-value pairs + size_t offset = 0; + while (offset < header.bytesOfKeyValueData) + { + if (offset + sizeof(uint32_t) > header.bytesOfKeyValueData) + break; + + uint32_t keyAndValueByteSize; + memcpy(&keyAndValueByteSize, kvData.data() + offset, sizeof(uint32_t)); + if (header.endianness != MyEndian) + osg::swapBytes4(reinterpret_cast(&keyAndValueByteSize)); + offset += sizeof(uint32_t); + + if (offset + keyAndValueByteSize > header.bytesOfKeyValueData) + break; + + // Find the null terminator to separate key from value + std::string key; + size_t keyStart = offset; + size_t keyEnd = keyStart; + while (keyEnd < offset + keyAndValueByteSize && kvData[keyEnd] != '\0') + keyEnd++; + + if (keyEnd < offset + keyAndValueByteSize) + { + key = std::string(kvData.data() + keyStart, keyEnd - keyStart); + + // Check for KTXorientation (note: some files incorrectly use KTXOrientation) + if (key == "KTXorientation" || key == "KTXOrientation") + { + size_t valueStart = keyEnd + 1; + size_t valueSize = keyAndValueByteSize - (valueStart - keyStart); + ktxOrientation = std::string(kvData.data() + valueStart, valueSize); + + // Remove any trailing null bytes + size_t nullPos = ktxOrientation.find('\0'); + if (nullPos != std::string::npos) + ktxOrientation = ktxOrientation.substr(0, nullPos); + + OSG_INFO << "Found KTXorientation: " << ktxOrientation << std::endl; + } + } + + // Align to 4 bytes + offset += keyAndValueByteSize; + uint32_t padding = (4 - (keyAndValueByteSize % 4)) % 4; + offset += padding; + } + } + else + { + // No key-value data, skip + fin.ignore(0); + } + + // Check if this is an ASTC texture + bool isASTCTexture = (header.glInternalFormat >= 0x93B0 && header.glInternalFormat <= 0x93DD); + + // For ASTC textures, validate orientation + if (isASTCTexture) + { + // ASTC textures must have S=r,T=u orientation (OpenGL native, bottom-left origin) + // If no KTXorientation is specified, we assume the default which is incorrect for ASTC + if (ktxOrientation.empty()) + { + OSG_WARN << "ASTC texture in KTX file lacks KTXorientation metadata. " + << "ASTC textures require explicit KTXorientation=S=r,T=u" << std::endl; + return ReadResult(ReadResult::FILE_NOT_HANDLED); + } + else if (ktxOrientation != "S=r,T=u" && ktxOrientation != "S=r,T=u,R=o") + { + OSG_WARN << "ASTC texture in KTX file has incompatible orientation: " << ktxOrientation + << ". ASTC textures require KTXorientation=S=r,T=u" << std::endl; + return ReadResult(ReadResult::FILE_NOT_HANDLED); + } + } uint32_t imageSize; uint32_t totalImageSize = fileLength - @@ -210,8 +300,18 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin header.glInternalFormat, header.glFormat, header.glType, totalImageData, osg::Image::USE_NEW_DELETE); - // KTX textures are stored in their original coordinate system (TOP_LEFT) - image->setOrigin(osg::Image::TOP_LEFT); + // Set origin based on KTXorientation metadata + // S=r,T=u means OpenGL orientation (bottom-left origin) + // S=r,T=d means standard image orientation (top-left origin) + if (ktxOrientation == "S=r,T=u" || ktxOrientation == "S=r,T=u,R=o") + { + image->setOrigin(osg::Image::BOTTOM_LEFT); + } + else + { + // Default to TOP_LEFT for S=r,T=d or when no orientation is specified + image->setOrigin(osg::Image::TOP_LEFT); + } if (header.numberOfMipmapLevels > 1) image->setMipmapLevels(mipmapData); From bdc9762156c6ec5e2d78e19123e655a5da94f4e6 Mon Sep 17 00:00:00 2001 From: bmdhacks Date: Fri, 5 Sep 2025 08:43:25 -0700 Subject: [PATCH 3/5] make badly aligned KTX textures just a warning --- src/osgPlugins/ktx/ReaderWriterKTX.cpp | 30 ++++++++++---------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/osgPlugins/ktx/ReaderWriterKTX.cpp b/src/osgPlugins/ktx/ReaderWriterKTX.cpp index 3560c3760ba..c59503b03d5 100644 --- a/src/osgPlugins/ktx/ReaderWriterKTX.cpp +++ b/src/osgPlugins/ktx/ReaderWriterKTX.cpp @@ -186,26 +186,18 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin fin.ignore(0); } - // Check if this is an ASTC texture - bool isASTCTexture = (header.glInternalFormat >= 0x93B0 && header.glInternalFormat <= 0x93DD); - - // For ASTC textures, validate orientation - if (isASTCTexture) + // Warn about orientation issues + if (ktxOrientation.empty()) { - // ASTC textures must have S=r,T=u orientation (OpenGL native, bottom-left origin) - // If no KTXorientation is specified, we assume the default which is incorrect for ASTC - if (ktxOrientation.empty()) - { - OSG_WARN << "ASTC texture in KTX file lacks KTXorientation metadata. " - << "ASTC textures require explicit KTXorientation=S=r,T=u" << std::endl; - return ReadResult(ReadResult::FILE_NOT_HANDLED); - } - else if (ktxOrientation != "S=r,T=u" && ktxOrientation != "S=r,T=u,R=o") - { - OSG_WARN << "ASTC texture in KTX file has incompatible orientation: " << ktxOrientation - << ". ASTC textures require KTXorientation=S=r,T=u" << std::endl; - return ReadResult(ReadResult::FILE_NOT_HANDLED); - } + OSG_WARN << "KTX file lacks KTXorientation metadata. OpenSceneGraph expects KTX textures " + << "in OpenGL orientation (S=r,T=u). Textures created for DirectX/Vulkan (S=r,T=d) " + << "may appear vertically flipped." << std::endl; + } + else if (ktxOrientation == "S=r,T=d" || ktxOrientation == "S=r,T=d,R=i" || ktxOrientation == "S=r,T=d,R=o") + { + OSG_WARN << "KTX file has DirectX/Vulkan orientation (" << ktxOrientation + << "). OpenSceneGraph expects OpenGL orientation (S=r,T=u). " + << "Texture may appear vertically flipped." << std::endl; } uint32_t imageSize; From ccd2ece79c6b2eb53a03ce3ce7dc10c778c2ef2a Mon Sep 17 00:00:00 2001 From: bmdhacks Date: Wed, 10 Sep 2025 15:42:09 -0700 Subject: [PATCH 4/5] store ktx metdata in the images user data structure and remove the warning about orientation. --- src/osgPlugins/ktx/ReaderWriterKTX.cpp | 58 +++++++++++++------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/osgPlugins/ktx/ReaderWriterKTX.cpp b/src/osgPlugins/ktx/ReaderWriterKTX.cpp index c59503b03d5..2fd8b043ed2 100644 --- a/src/osgPlugins/ktx/ReaderWriterKTX.cpp +++ b/src/osgPlugins/ktx/ReaderWriterKTX.cpp @@ -13,11 +13,13 @@ #include "ReaderWriterKTX.h" #include +#include #include #include #include #include #include +#include // Macro similar to what's in FLT/TRP plugins (except it uses wide char under Windows if OSG_USE_UTF8_FILENAME) #if defined(_WIN32) @@ -117,9 +119,8 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin if (header.numberOfMipmapLevels == 0) header.numberOfMipmapLevels = 1; - // Parse key-value metadata to check for KTXorientation - bool hasValidOrientation = true; // Default to true for non-ASTC or when no metadata - std::string ktxOrientation; + // Parse key-value metadata + std::map ktxMetadata; if (header.bytesOfKeyValueData > 0) { @@ -158,20 +159,18 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin { key = std::string(kvData.data() + keyStart, keyEnd - keyStart); - // Check for KTXorientation (note: some files incorrectly use KTXOrientation) - if (key == "KTXorientation" || key == "KTXOrientation") - { - size_t valueStart = keyEnd + 1; - size_t valueSize = keyAndValueByteSize - (valueStart - keyStart); - ktxOrientation = std::string(kvData.data() + valueStart, valueSize); + // Extract the value (everything after the null terminator) + size_t valueStart = keyEnd + 1; + size_t valueSize = keyAndValueByteSize - (valueStart - keyStart); + std::string value(kvData.data() + valueStart, valueSize); - // Remove any trailing null bytes - size_t nullPos = ktxOrientation.find('\0'); - if (nullPos != std::string::npos) - ktxOrientation = ktxOrientation.substr(0, nullPos); + // Remove any trailing null bytes from the value + size_t nullPos = value.find('\0'); + if (nullPos != std::string::npos) + value = value.substr(0, nullPos); - OSG_INFO << "Found KTXorientation: " << ktxOrientation << std::endl; - } + // Store the metadata + ktxMetadata[key] = value; } // Align to 4 bytes @@ -186,20 +185,6 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin fin.ignore(0); } - // Warn about orientation issues - if (ktxOrientation.empty()) - { - OSG_WARN << "KTX file lacks KTXorientation metadata. OpenSceneGraph expects KTX textures " - << "in OpenGL orientation (S=r,T=u). Textures created for DirectX/Vulkan (S=r,T=d) " - << "may appear vertically flipped." << std::endl; - } - else if (ktxOrientation == "S=r,T=d" || ktxOrientation == "S=r,T=d,R=i" || ktxOrientation == "S=r,T=d,R=o") - { - OSG_WARN << "KTX file has DirectX/Vulkan orientation (" << ktxOrientation - << "). OpenSceneGraph expects OpenGL orientation (S=r,T=u). " - << "Texture may appear vertically flipped." << std::endl; - } - uint32_t imageSize; uint32_t totalImageSize = fileLength - (sizeof(KTXTexHeader) + header.bytesOfKeyValueData + @@ -295,7 +280,13 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin // Set origin based on KTXorientation metadata // S=r,T=u means OpenGL orientation (bottom-left origin) // S=r,T=d means standard image orientation (top-left origin) - if (ktxOrientation == "S=r,T=u" || ktxOrientation == "S=r,T=u,R=o") + // Note: some files incorrectly use KTXOrientation with capital O + auto orientIt = ktxMetadata.find("KTXorientation"); + if (orientIt == ktxMetadata.end()) + orientIt = ktxMetadata.find("KTXOrientation"); + + if (orientIt != ktxMetadata.end() && + (orientIt->second == "S=r,T=u" || orientIt->second == "S=r,T=u,R=o")) { image->setOrigin(osg::Image::BOTTOM_LEFT); } @@ -308,6 +299,13 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin if (header.numberOfMipmapLevels > 1) image->setMipmapLevels(mipmapData); + // Store all KTX metadata in the image's userdata with "KTX:" prefix + // This avoids conflicts with other OSG metadata + for (const auto& kv : ktxMetadata) + { + image->setUserValue("KTX:" + kv.first, kv.second); + } + return image.get(); } From f64cc4e14a4568a4d0fb4c519a5c73e80de9fd45 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 11 Oct 2025 17:17:59 -0700 Subject: [PATCH 5/5] remove a noop --- src/osgPlugins/ktx/ReaderWriterKTX.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/osgPlugins/ktx/ReaderWriterKTX.cpp b/src/osgPlugins/ktx/ReaderWriterKTX.cpp index 2fd8b043ed2..55779baddaa 100644 --- a/src/osgPlugins/ktx/ReaderWriterKTX.cpp +++ b/src/osgPlugins/ktx/ReaderWriterKTX.cpp @@ -179,11 +179,6 @@ osgDB::ReaderWriter::ReadResult ReaderWriterKTX::readKTXStream(std::istream& fin offset += padding; } } - else - { - // No key-value data, skip - fin.ignore(0); - } uint32_t imageSize; uint32_t totalImageSize = fileLength -