From de9de576fadf275fd717636372e627e7b4210c5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:08:11 +0000 Subject: [PATCH 1/4] Initial plan From b3bac7c1ea98632b839e6173f094100b3a4f74af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:20:36 +0000 Subject: [PATCH 2/4] Add KTX1 texture container support for reading compressed and uncompressed textures Co-authored-by: kaetemi <1581053+kaetemi@users.noreply.github.com> --- nel/include/nel/misc/bitmap.h | 27 ++- nel/src/misc/bitmap.cpp | 38 ++++ nel/src/misc/bitmap_ktx.cpp | 357 ++++++++++++++++++++++++++++++++++ 3 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 nel/src/misc/bitmap_ktx.cpp diff --git a/nel/include/nel/misc/bitmap.h b/nel/include/nel/misc/bitmap.h index 1f691a8593..e9681fc86e 100644 --- a/nel/include/nel/misc/bitmap.h +++ b/nel/include/nel/misc/bitmap.h @@ -48,6 +48,7 @@ const uint32 DXT_HEADER = NL_MAKEFOURCC('D', 'X', 'T', '\0'); const uint32 PNG_HEADER = NL_MAKEFOURCC(0x89, 'P', 'N', 'G'); const uint32 JPG_HEADER = NL_MAKEFOURCC(0xff, 0xd8, 0xff, 0xe0); const uint32 GIF_HEADER = NL_MAKEFOURCC('G', 'I', 'F', '8'); +const uint32 KTX_HEADER = NL_MAKEFOURCC(0xAB, 'K', 'T', 'X'); // dwLinearSize is valid @@ -145,6 +146,16 @@ private : uint8 readGIF( NLMISC::IStream &f ); + /** + * Read a KTX1 from an IStream. + * KTX files can contain compressed (DXTC) or uncompressed texture data. + * \param f IStream (must be a reading stream) + * \param mipMapSkip number of mipmaps to skip + * \return image depth if succeed, 0 else + */ + uint8 readKTX( NLMISC::IStream &f, uint mipMapSkip ); + + /** * Change bitmap format * @@ -330,11 +341,12 @@ private : void swap(CBitmap &other); /** - * Read a bitmap(TGA, JPEG, PNG or DDS) from an IStream. - * Bitmap supported are DDS (DXTC1, DXTC1 with Alpha, DXTC3, DXTC5), PNG, JPEG and - * uncompressed TGA (24 and 32 bits). + * Read a bitmap(TGA, JPEG, PNG, DDS or KTX) from an IStream. + * Bitmap supported are DDS (DXTC1, DXTC1 with Alpha, DXTC3, DXTC5), + * KTX (DXTC1, DXTC3, DXTC5, RGBA, Luminance, Alpha), + * PNG, JPEG and uncompressed TGA (24 and 32 bits). * \param IStream The stream must be in reading mode. - * \param mipMapSkip if the file is a DDS with mipMap. N=mipMapSkip mipmaps are skipped. + * \param mipMapSkip if the file is a DDS or KTX with mipMap. N=mipMapSkip mipmaps are skipped. * \return image depth (24 or 32), or 0 if load failed * \throw ESeekFailed : seek has failed */ @@ -342,9 +354,10 @@ private : /** - * Determinate the bitmap size from a bitmap(TGA or DDS) from an IStream. load just header of the file. - * Bitmap supported are DDS (DXTC1, DXTC1 with Alpha, DXTC3, DXTC5), PNG, JPEG and - * uncompressed TGA (24 and 32 bits). + * Determinate the bitmap size from a bitmap(TGA, DDS or KTX) from an IStream. load just header of the file. + * Bitmap supported are DDS (DXTC1, DXTC1 with Alpha, DXTC3, DXTC5), + * KTX (DXTC1, DXTC3, DXTC5, RGBA, Luminance, Alpha), + * PNG, JPEG and uncompressed TGA (24 and 32 bits). * NB: at the end, f is seeked to begin. * \param IStream The stream must be in reading mode. * \param width the width of the image. 0 if fails. diff --git a/nel/src/misc/bitmap.cpp b/nel/src/misc/bitmap.cpp index 59360adb6e..5952b03388 100644 --- a/nel/src/misc/bitmap.cpp +++ b/nel/src/misc/bitmap.cpp @@ -165,6 +165,17 @@ uint8 CBitmap::load(NLMISC::IStream &f, uint mipMapSkip) } #endif // USE_GIF + if (fileType == KTX_HEADER) + { +#ifdef NEL_ALL_BITMAP_WHITE + uint8 result = readKTX(f, mipMapSkip); + MakeWhite (*this); + return result; +#else // NEL_ALL_BITMAP_WHITE + return readKTX(f, mipMapSkip); +#endif // NEL_ALL_BITMAP_WHITE + } + // assuming it's TGA NLMISC::IStream::TSeekOrigin origin= f.begin; if(!f.seek (0, origin)) @@ -3667,6 +3678,33 @@ void CBitmap::loadSize(NLMISC::IStream &f, uint32 &retWidth, uint32 &retHeight) retWidth = lsWidth; retHeight = lsHeight; } + else if(fileType == KTX_HEADER) + { + // skip remaining 8 bytes of KTX identifier + f.seek(8, IStream::current); + + // read endianness indicator + uint32 endianness; + f.serial(endianness); + bool mustSwap = (endianness == 0x01020304); + + // skip glType, glTypeSize, glFormat, glInternalFormat, glBaseInternalFormat (5 uint32s) + f.seek(5 * 4, IStream::current); + + // read pixelWidth and pixelHeight + uint32 w, h; + f.serial(w); + f.serial(h); + + if (mustSwap) + { + NLMISC_BSWAP32(w); + NLMISC_BSWAP32(h); + } + + retWidth = w; + retHeight = h; + } // assuming it's TGA else { diff --git a/nel/src/misc/bitmap_ktx.cpp b/nel/src/misc/bitmap_ktx.cpp new file mode 100644 index 0000000000..9409f527db --- /dev/null +++ b/nel/src/misc/bitmap_ktx.cpp @@ -0,0 +1,357 @@ +// NeL - MMORPG Framework +// Copyright (C) 2010-2021 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#include "stdmisc.h" + +#include "nel/misc/bitmap.h" +#include "nel/misc/stream.h" + +using namespace std; + +#ifdef DEBUG_NEW + #define new DEBUG_NEW +#endif + +namespace NLMISC +{ + +// KTX1 file format identifier bytes [4..11] (first 4 bytes already consumed as KTX_HEADER) +static const uint8 KTX_ID_REST[8] = { 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + +// GL constants for texture formats used in KTX files +static const uint32 GL_UNSIGNED_BYTE_ = 0x1401; +static const uint32 GL_ALPHA_ = 0x1906; +static const uint32 GL_RGB_ = 0x1907; +static const uint32 GL_RGBA_ = 0x1908; +static const uint32 GL_LUMINANCE_ = 0x1909; +static const uint32 GL_LUMINANCE_ALPHA_ = 0x190A; +static const uint32 GL_COMPRESSED_RGB_S3TC_DXT1_EXT_ = 0x83F0; +static const uint32 GL_COMPRESSED_RGBA_S3TC_DXT1_EXT_ = 0x83F1; +static const uint32 GL_COMPRESSED_RGBA_S3TC_DXT3_EXT_ = 0x83F2; +static const uint32 GL_COMPRESSED_RGBA_S3TC_DXT5_EXT_ = 0x83F3; +static const uint32 GL_ETC1_RGB8_OES_ = 0x8D64; + +static void ktxSwap32(uint32 &val) +{ + val = ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | ((val & 0xFF000000) >> 24); +} + +/*-------------------------------------------------------------------*\ + readKTX +\*-------------------------------------------------------------------*/ +uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) +{ + /* *********************************************** + * WARNING: This Class/Method must be thread-safe (ctor/dtor/serial): no static access for instance + * It can be loaded/called through CAsyncFileManager for instance + * ***********************************************/ + + //-------------- Read and validate KTX identifier + + // First 4 bytes already consumed by load() as KTX_HEADER + // Read and verify remaining 8 bytes of the KTX1 identifier + uint8 idRest[8]; + f.serialBuffer(idRest, 8); + if (memcmp(idRest, KTX_ID_REST, 8) != 0) + { + nlwarning("Invalid KTX identifier"); + return 0; + } + + //-------------- Read KTX header fields + + uint32 endianness; + uint32 glType; + uint32 glTypeSize; + uint32 glFormat; + uint32 glInternalFormat; + uint32 glBaseInternalFormat; + uint32 pixelWidth; + uint32 pixelHeight; + uint32 pixelDepth; + uint32 numberOfArrayElements; + uint32 numberOfFaces; + uint32 numberOfMipmapLevels; + uint32 bytesOfKeyValueData; + + f.serial(endianness); + bool mustSwap = (endianness == 0x01020304); + + f.serial(glType); + f.serial(glTypeSize); + f.serial(glFormat); + f.serial(glInternalFormat); + f.serial(glBaseInternalFormat); + f.serial(pixelWidth); + f.serial(pixelHeight); + f.serial(pixelDepth); + f.serial(numberOfArrayElements); + f.serial(numberOfFaces); + f.serial(numberOfMipmapLevels); + f.serial(bytesOfKeyValueData); + + if (mustSwap) + { + ktxSwap32(glType); + ktxSwap32(glTypeSize); + ktxSwap32(glFormat); + ktxSwap32(glInternalFormat); + ktxSwap32(glBaseInternalFormat); + ktxSwap32(pixelWidth); + ktxSwap32(pixelHeight); + ktxSwap32(pixelDepth); + ktxSwap32(numberOfArrayElements); + ktxSwap32(numberOfFaces); + ktxSwap32(numberOfMipmapLevels); + ktxSwap32(bytesOfKeyValueData); + } + + //-------------- Validate basic constraints + + // Only 2D textures supported (no 3D, no arrays, no cubemaps) + if (pixelDepth > 1) + { + nlwarning("KTX: 3D textures not supported"); + return 0; + } + if (numberOfArrayElements > 1) + { + nlwarning("KTX: texture arrays not supported"); + return 0; + } + if (numberOfFaces > 1) + { + nlwarning("KTX: cubemap textures not supported"); + return 0; + } + + if (pixelWidth == 0 || pixelHeight == 0) + { + nlwarning("KTX: invalid texture dimensions"); + return 0; + } + + // 0 means 1 mipmap level (auto-generate) + if (numberOfMipmapLevels == 0) + numberOfMipmapLevels = 1; + + //-------------- Skip key/value data + if (bytesOfKeyValueData > 0) + { + if (!f.seek(bytesOfKeyValueData, IStream::current)) + { + nlwarning("KTX: failed to skip key/value data"); + return 0; + } + } + + //-------------- Determine pixel format + + bool compressed = (glType == 0 && glFormat == 0); + uint8 imageDepth = 0; + + if (compressed) + { + switch (glInternalFormat) + { + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT_: + PixelFormat = DXTC1; + imageDepth = 24; + break; + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT_: + PixelFormat = DXTC1Alpha; + imageDepth = 32; + break; + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT_: + PixelFormat = DXTC3; + imageDepth = 32; + break; + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT_: + PixelFormat = DXTC5; + imageDepth = 32; + break; + case GL_ETC1_RGB8_OES_: + nlwarning("KTX: ETC1 compressed format not yet supported by bitmap loader"); + return 0; + default: + nlwarning("KTX: unsupported compressed glInternalFormat 0x%x", glInternalFormat); + return 0; + } + } + else + { + // Uncompressed formats + switch (glBaseInternalFormat) + { + case GL_RGBA_: + PixelFormat = RGBA; + imageDepth = 32; + break; + case GL_RGB_: + // We will expand RGB to RGBA + PixelFormat = RGBA; + imageDepth = 24; + break; + case GL_LUMINANCE_: + if (_LoadGrayscaleAsAlpha) + { + PixelFormat = Alpha; + imageDepth = 8; + } + else + { + PixelFormat = Luminance; + imageDepth = 8; + } + break; + case GL_ALPHA_: + PixelFormat = Alpha; + imageDepth = 8; + break; + case GL_LUMINANCE_ALPHA_: + PixelFormat = AlphaLuminance; + imageDepth = 16; + break; + default: + nlwarning("KTX: unsupported uncompressed glBaseInternalFormat 0x%x", glBaseInternalFormat); + return 0; + } + } + + //-------------- Setup dimensions + + _Width = pixelWidth; + _Height = pixelHeight; + _MipMapCount = (uint8)numberOfMipmapLevels; + if (_MipMapCount > MAX_MIPMAP) + _MipMapCount = MAX_MIPMAP; + + //-------------- Handle mipmap skip for compressed formats + + if (compressed && _MipMapCount > 1 && mipMapSkip > 0) + { + uint minSizeLevel = min(_Width, _Height); + minSizeLevel = getPowerOf2(minSizeLevel); + + if (minSizeLevel > 2) + { + mipMapSkip = min(mipMapSkip, minSizeLevel - 2); + + while (mipMapSkip > 0 && _MipMapCount > 1) + { + // Read imageSize for this mipmap level + uint32 imageSize; + f.serial(imageSize); + if (mustSwap) ktxSwap32(imageSize); + + // Skip the data + padding + uint32 imageSizePadded = (imageSize + 3) & ~3u; + if (!f.seek(imageSizePadded, IStream::current)) + { + nlwarning("KTX: failed to skip mipmap data"); + return 0; + } + + _Width >>= 1; + if (_Width == 0) _Width = 1; + _Height >>= 1; + if (_Height == 0) _Height = 1; + _MipMapCount--; + mipMapSkip--; + } + } + } + + //-------------- Read mipmap levels + + for (uint8 m = 0; m < _MipMapCount; m++) + { + uint32 imageSize; + f.serial(imageSize); + if (mustSwap) ktxSwap32(imageSize); + + uint32 w = max(_Width >> m, 1u); + uint32 h = max(_Height >> m, 1u); + + if (compressed) + { + // For DXTC compressed data, store directly + _Data[m].resize(imageSize); + f.serialBuffer(_Data[m].getPtr(), imageSize); + } + else + { + // For uncompressed data, read and convert to our internal format + uint32 srcBpp = 0; + switch (glBaseInternalFormat) + { + case GL_RGBA_: srcBpp = 4; break; + case GL_RGB_: srcBpp = 3; break; + case GL_LUMINANCE_ALPHA_: srcBpp = 2; break; + case GL_LUMINANCE_: srcBpp = 1; break; + case GL_ALPHA_: srcBpp = 1; break; + default: srcBpp = 4; break; + } + + uint32 srcSize = w * h * srcBpp; + if (imageSize < srcSize) + { + nlwarning("KTX: imageSize %u too small for %ux%u with %u bpp", imageSize, w, h, srcBpp); + return 0; + } + + vector srcData(imageSize); + f.serialBuffer(&srcData[0], imageSize); + + uint32 dstBpp = bitPerPixels[PixelFormat] / 8; + uint32 dstSize = w * h * dstBpp; + _Data[m].resize(dstSize); + + if (glBaseInternalFormat == GL_RGB_) + { + // Expand RGB to RGBA + for (uint32 p = 0; p < w * h; p++) + { + _Data[m][p * 4 + 0] = srcData[p * 3 + 0]; + _Data[m][p * 4 + 1] = srcData[p * 3 + 1]; + _Data[m][p * 4 + 2] = srcData[p * 3 + 2]; + _Data[m][p * 4 + 3] = 255; + } + } + else + { + // Direct copy (RGBA, Luminance, Alpha, LuminanceAlpha) + memcpy(_Data[m].getPtr(), &srcData[0], dstSize); + } + } + + // Skip padding to 4-byte boundary + uint32 imageSizePadded = (imageSize + 3) & ~3u; + uint32 padding = imageSizePadded - imageSize; + if (padding > 0) + { + if (!f.seek(padding, IStream::current)) + { + nlwarning("KTX: failed to skip padding"); + return 0; + } + } + } + + return imageDepth; +} + +} // namespace NLMISC From 973b48c1a531c85f3e42564033cea29e257b7b9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:22:02 +0000 Subject: [PATCH 3/4] Refactor: extract srcBpp computation from mipmap read loop in readKTX Co-authored-by: kaetemi <1581053+kaetemi@users.noreply.github.com> --- nel/src/misc/bitmap_ktx.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/nel/src/misc/bitmap_ktx.cpp b/nel/src/misc/bitmap_ktx.cpp index 9409f527db..1358a5be88 100644 --- a/nel/src/misc/bitmap_ktx.cpp +++ b/nel/src/misc/bitmap_ktx.cpp @@ -162,6 +162,7 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) bool compressed = (glType == 0 && glFormat == 0); uint8 imageDepth = 0; + uint32 srcBpp = 0; // source bytes per pixel for uncompressed formats if (compressed) { @@ -199,11 +200,13 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) case GL_RGBA_: PixelFormat = RGBA; imageDepth = 32; + srcBpp = 4; break; case GL_RGB_: // We will expand RGB to RGBA PixelFormat = RGBA; imageDepth = 24; + srcBpp = 3; break; case GL_LUMINANCE_: if (_LoadGrayscaleAsAlpha) @@ -216,14 +219,17 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) PixelFormat = Luminance; imageDepth = 8; } + srcBpp = 1; break; case GL_ALPHA_: PixelFormat = Alpha; imageDepth = 8; + srcBpp = 1; break; case GL_LUMINANCE_ALPHA_: PixelFormat = AlphaLuminance; imageDepth = 16; + srcBpp = 2; break; default: nlwarning("KTX: unsupported uncompressed glBaseInternalFormat 0x%x", glBaseInternalFormat); @@ -295,17 +301,6 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) else { // For uncompressed data, read and convert to our internal format - uint32 srcBpp = 0; - switch (glBaseInternalFormat) - { - case GL_RGBA_: srcBpp = 4; break; - case GL_RGB_: srcBpp = 3; break; - case GL_LUMINANCE_ALPHA_: srcBpp = 2; break; - case GL_LUMINANCE_: srcBpp = 1; break; - case GL_ALPHA_: srcBpp = 1; break; - default: srcBpp = 4; break; - } - uint32 srcSize = w * h * srcBpp; if (imageSize < srcSize) { From 469152d417d74f42ee96fd6ff8fd3512cd4c04bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:22:38 +0000 Subject: [PATCH 4/4] Fix KTX1 spec compliance: raw reads, endianness validation, 1D textures, row alignment Co-authored-by: kaetemi <1581053+kaetemi@users.noreply.github.com> --- nel/src/misc/bitmap.cpp | 13 ++--- nel/src/misc/bitmap_ktx.cpp | 101 +++++++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 35 deletions(-) diff --git a/nel/src/misc/bitmap.cpp b/nel/src/misc/bitmap.cpp index 5952b03388..d8b9974bea 100644 --- a/nel/src/misc/bitmap.cpp +++ b/nel/src/misc/bitmap.cpp @@ -3683,18 +3683,18 @@ void CBitmap::loadSize(NLMISC::IStream &f, uint32 &retWidth, uint32 &retHeight) // skip remaining 8 bytes of KTX identifier f.seek(8, IStream::current); - // read endianness indicator + // read endianness indicator using raw read (KTX has its own endianness handling) uint32 endianness; - f.serial(endianness); + f.serialBuffer((uint8 *)&endianness, 4); bool mustSwap = (endianness == 0x01020304); // skip glType, glTypeSize, glFormat, glInternalFormat, glBaseInternalFormat (5 uint32s) f.seek(5 * 4, IStream::current); - // read pixelWidth and pixelHeight + // read pixelWidth and pixelHeight using raw reads uint32 w, h; - f.serial(w); - f.serial(h); + f.serialBuffer((uint8 *)&w, 4); + f.serialBuffer((uint8 *)&h, 4); if (mustSwap) { @@ -3703,7 +3703,8 @@ void CBitmap::loadSize(NLMISC::IStream &f, uint32 &retWidth, uint32 &retHeight) } retWidth = w; - retHeight = h; + // Per spec: pixelHeight 0 means 1D texture, treat as height 1 + retHeight = (h == 0) ? 1 : h; } // assuming it's TGA else diff --git a/nel/src/misc/bitmap_ktx.cpp b/nel/src/misc/bitmap_ktx.cpp index 1358a5be88..1b19b514bf 100644 --- a/nel/src/misc/bitmap_ktx.cpp +++ b/nel/src/misc/bitmap_ktx.cpp @@ -49,6 +49,13 @@ static void ktxSwap32(uint32 &val) val = ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | ((val & 0xFF000000) >> 24); } +// Read a raw uint32 from stream without any platform byte-swapping. +// KTX files have their own endianness handling via the endianness field. +static void ktxReadRaw32(NLMISC::IStream &f, uint32 &val) +{ + f.serialBuffer((uint8 *)&val, 4); +} + /*-------------------------------------------------------------------*\ readKTX \*-------------------------------------------------------------------*/ @@ -72,6 +79,8 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) } //-------------- Read KTX header fields + // Use raw reads (serialBuffer) to avoid platform byte-swapping by IStream::serial(). + // KTX files can be either little-endian or big-endian, determined by the endianness field. uint32 endianness; uint32 glType; @@ -87,21 +96,35 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) uint32 numberOfMipmapLevels; uint32 bytesOfKeyValueData; - f.serial(endianness); - bool mustSwap = (endianness == 0x01020304); - - f.serial(glType); - f.serial(glTypeSize); - f.serial(glFormat); - f.serial(glInternalFormat); - f.serial(glBaseInternalFormat); - f.serial(pixelWidth); - f.serial(pixelHeight); - f.serial(pixelDepth); - f.serial(numberOfArrayElements); - f.serial(numberOfFaces); - f.serial(numberOfMipmapLevels); - f.serial(bytesOfKeyValueData); + ktxReadRaw32(f, endianness); + + bool mustSwap; + if (endianness == 0x04030201) + { + mustSwap = false; + } + else if (endianness == 0x01020304) + { + mustSwap = true; + } + else + { + nlwarning("KTX: invalid endianness field 0x%08x", endianness); + return 0; + } + + ktxReadRaw32(f, glType); + ktxReadRaw32(f, glTypeSize); + ktxReadRaw32(f, glFormat); + ktxReadRaw32(f, glInternalFormat); + ktxReadRaw32(f, glBaseInternalFormat); + ktxReadRaw32(f, pixelWidth); + ktxReadRaw32(f, pixelHeight); + ktxReadRaw32(f, pixelDepth); + ktxReadRaw32(f, numberOfArrayElements); + ktxReadRaw32(f, numberOfFaces); + ktxReadRaw32(f, numberOfMipmapLevels); + ktxReadRaw32(f, bytesOfKeyValueData); if (mustSwap) { @@ -138,12 +161,16 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) return 0; } - if (pixelWidth == 0 || pixelHeight == 0) + if (pixelWidth == 0) { - nlwarning("KTX: invalid texture dimensions"); + nlwarning("KTX: invalid texture width (0)"); return 0; } + // Per spec: for 1D textures pixelHeight must be 0, treat as 1 + if (pixelHeight == 0) + pixelHeight = 1; + // 0 means 1 mipmap level (auto-generate) if (numberOfMipmapLevels == 0) numberOfMipmapLevels = 1; @@ -260,7 +287,7 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) { // Read imageSize for this mipmap level uint32 imageSize; - f.serial(imageSize); + ktxReadRaw32(f, imageSize); if (mustSwap) ktxSwap32(imageSize); // Skip the data + padding @@ -286,7 +313,7 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) for (uint8 m = 0; m < _MipMapCount; m++) { uint32 imageSize; - f.serial(imageSize); + ktxReadRaw32(f, imageSize); if (mustSwap) ktxSwap32(imageSize); uint32 w = max(_Width >> m, 1u); @@ -300,11 +327,14 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) } else { - // For uncompressed data, read and convert to our internal format - uint32 srcSize = w * h * srcBpp; + // Per KTX spec: uncompressed pixel data uses GL_UNPACK_ALIGNMENT of 4, + // meaning each row is padded to a multiple of 4 bytes. + uint32 rowBytes = w * srcBpp; + uint32 rowStride = (rowBytes + 3) & ~3u; + uint32 srcSize = rowStride * h; if (imageSize < srcSize) { - nlwarning("KTX: imageSize %u too small for %ux%u with %u bpp", imageSize, w, h, srcBpp); + nlwarning("KTX: imageSize %u too small for %ux%u with %u bpp (expected %u)", imageSize, w, h, srcBpp, srcSize); return 0; } @@ -317,18 +347,31 @@ uint8 CBitmap::readKTX( NLMISC::IStream &f, uint mipMapSkip ) if (glBaseInternalFormat == GL_RGB_) { - // Expand RGB to RGBA - for (uint32 p = 0; p < w * h; p++) + // Expand RGB to RGBA, accounting for row stride + for (uint32 y = 0; y < h; y++) + { + for (uint32 x = 0; x < w; x++) + { + uint32 srcOffset = y * rowStride + x * 3; + uint32 dstOffset = (y * w + x) * 4; + _Data[m][dstOffset + 0] = srcData[srcOffset + 0]; + _Data[m][dstOffset + 1] = srcData[srcOffset + 1]; + _Data[m][dstOffset + 2] = srcData[srcOffset + 2]; + _Data[m][dstOffset + 3] = 255; + } + } + } + else if (rowBytes != rowStride) + { + // Copy row by row to strip padding + for (uint32 y = 0; y < h; y++) { - _Data[m][p * 4 + 0] = srcData[p * 3 + 0]; - _Data[m][p * 4 + 1] = srcData[p * 3 + 1]; - _Data[m][p * 4 + 2] = srcData[p * 3 + 2]; - _Data[m][p * 4 + 3] = 255; + memcpy(_Data[m].getPtr() + y * w * dstBpp, &srcData[y * rowStride], rowBytes); } } else { - // Direct copy (RGBA, Luminance, Alpha, LuminanceAlpha) + // No row padding, direct copy (RGBA, Luminance, Alpha, LuminanceAlpha) memcpy(_Data[m].getPtr(), &srcData[0], dstSize); } }