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..d8b9974bea 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,34 @@ 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 using raw read (KTX has its own endianness handling)
+ uint32 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 using raw reads
+ uint32 w, h;
+ f.serialBuffer((uint8 *)&w, 4);
+ f.serialBuffer((uint8 *)&h, 4);
+
+ if (mustSwap)
+ {
+ NLMISC_BSWAP32(w);
+ NLMISC_BSWAP32(h);
+ }
+
+ retWidth = w;
+ // 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
new file mode 100644
index 0000000000..1b19b514bf
--- /dev/null
+++ b/nel/src/misc/bitmap_ktx.cpp
@@ -0,0 +1,395 @@
+// 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);
+}
+
+// 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
+\*-------------------------------------------------------------------*/
+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
+ // 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;
+ uint32 glTypeSize;
+ uint32 glFormat;
+ uint32 glInternalFormat;
+ uint32 glBaseInternalFormat;
+ uint32 pixelWidth;
+ uint32 pixelHeight;
+ uint32 pixelDepth;
+ uint32 numberOfArrayElements;
+ uint32 numberOfFaces;
+ uint32 numberOfMipmapLevels;
+ uint32 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)
+ {
+ 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)
+ {
+ 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;
+
+ //-------------- 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;
+ uint32 srcBpp = 0; // source bytes per pixel for uncompressed formats
+
+ 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;
+ srcBpp = 4;
+ break;
+ case GL_RGB_:
+ // We will expand RGB to RGBA
+ PixelFormat = RGBA;
+ imageDepth = 24;
+ srcBpp = 3;
+ break;
+ case GL_LUMINANCE_:
+ if (_LoadGrayscaleAsAlpha)
+ {
+ PixelFormat = Alpha;
+ imageDepth = 8;
+ }
+ else
+ {
+ 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);
+ 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;
+ ktxReadRaw32(f, 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;
+ ktxReadRaw32(f, 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
+ {
+ // 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 (expected %u)", imageSize, w, h, srcBpp, srcSize);
+ 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, 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++)
+ {
+ memcpy(_Data[m].getPtr() + y * w * dstBpp, &srcData[y * rowStride], rowBytes);
+ }
+ }
+ else
+ {
+ // No row padding, 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