#include "ktxutils.h" #include "bmpman/bmpman.h" #include "cfile/cfile.h" #define ETCDEC_IMPLEMENTATION 1 PUSH_SUPPRESS_WARNINGS #include "ktxutils/etcdec.h" POP_SUPPRESS_WARNINGS #ifdef WITH_OPENGL #include "glad/glad.h" #else // GLenum definitions in case of compiling without OpenGL // (KTX1 format saves texture type as an glInternalFormat) #define GL_COMPRESSED_RGB8_ETC2 0x9274 #define GL_COMPRESSED_SRGB8_ETC2 0x9275 #define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276 #define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277 #define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 #define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279 #endif //KTX1 Spec definition https://registry.khronos.org/KTX/specs/1.0/ktxspec.v1.html //ETC2 formats spec https://registry.khronos.org/DataFormat/specs/1.4/dataformat.1.4.inline.html#ETC2 /* KTX 1 Parser - Format supported by Etc2Comp and AMD Compressonator ETC2 support for the formats supported by AMD compressonator and their signed alternatives: ETC2_RGB / ETC2_SRGB -> 24 bit no alpha ETC2_RGBA1 / ETC2_SRGBA1 -> 24 bit with 1 bit punchthrough alpha ETC2_RGBA / ETC2_SRGBA -> 32 bit with 8 bit alpha */ static const uint8_t KTX_ID[12] = { 0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, '\r', '\n', 0x1A, '\n' }; uint32_t ktx_etc_block_size(const int internal_format) { switch (internal_format) { case GL_COMPRESSED_RGB8_ETC2: case GL_COMPRESSED_SRGB8_ETC2: case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: return 8; case GL_COMPRESSED_RGBA8_ETC2_EAC: case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: return 16; default: return 0; } } int ktx_map_ktx_format_to_gl_internal(const int ktx_format) { switch (ktx_format) { case KTX_ETC2_RGB: return GL_COMPRESSED_RGB8_ETC2; case KTX_ETC2_RGBA_EAC: return GL_COMPRESSED_RGBA8_ETC2_EAC; case KTX_ETC2_SRGB: return GL_COMPRESSED_SRGB8_ETC2; case KTX_ETC2_SRGBA_EAC: return GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC; case KTX_ETC2_RGB_A1: return GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2; case KTX_ETC2_SRGB_A1: return GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2; default: return 0; } } int ktx_map_gl_internal_to_bm_type(const int internal_format) { switch (internal_format) { case GL_COMPRESSED_RGB8_ETC2: return BM_TYPE_ETC2_RGB; case GL_COMPRESSED_SRGB8_ETC2: return BM_TYPE_ETC2_SRGB; case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: return BM_TYPE_ETC2_RGBA1; case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: return BM_TYPE_ETC2_SRGBA1; case GL_COMPRESSED_RGBA8_ETC2_EAC: return BM_TYPE_ETC2_RGBA_EAC; case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: return BM_TYPE_ETC2_SRGBA_EAC; default: return BM_TYPE_NONE; } } int ktx1_read_header(const char* filename, CFILE* img_cfp, int* w, int* h, int* bpp, int* c_type, int* mip_levels, size_t* total_size) { CFILE* cf; char real_name[MAX_FILENAME_LEN]; //copied from ddsutils if (img_cfp == nullptr) { // this better not happen.. ever Assert(filename != nullptr); // make sure there is an extension strcpy_s(real_name, filename); char* p = strchr(real_name, '.'); if (p) { *p = 0; } strcat_s(real_name, ".ktx"); // try to open the file cf = cfopen(real_name, "rb"); // file not found if (cf == nullptr) return KTX1_ERROR_INVALID_FILENAME; } else { cf = img_cfp; } KTX1_Header hdr{}; if (cfread(&hdr, 1, sizeof(hdr), cf) != sizeof(hdr)) { if (!img_cfp) cfclose(cf); mprintf(("ktx: read header error, parse header error. File: file='%s'\n", filename)); return KTX1_ERROR_BAD_HEADER; } if (memcmp(hdr.identifier, KTX_ID, 12) != 0 || hdr.endianness != 0x04030201) { if (!img_cfp) cfclose(cf); mprintf(("ktx: read header error, parse header error. File: file='%s'\n", filename)); return KTX1_ERROR_BAD_HEADER; } // Only 2D no arrays or cubemaps if (hdr.pixelHeight == 0 || hdr.pixelWidth == 0 || hdr.numberOfFaces > 1 || hdr.numberOfArrayElements > 0 || (hdr.pixelDepth > 1)) { if (!img_cfp) cfclose(cf); mprintf(("ktx: read header error, only 2D textures are supported. File: file='%s'\n", filename)); return KTX1_ERROR_UNSUPPORTED; } const bool isCompressed = (hdr.glType == 0 && hdr.glFormat == 0); if (!isCompressed) { //uncompressed if (!img_cfp) cfclose(cf); mprintf(("ktx: read header error, unsupported texture format on file: file='%s'\n", filename)); return KTX1_ERROR_INVALID_FORMAT; } const uint32_t fmt = hdr.glInternalFormat; const uint32_t blockBytes = ktx_etc_block_size(fmt); const int bm_ct = ktx_map_gl_internal_to_bm_type(fmt); if (blockBytes == 0 || bm_ct == BM_TYPE_NONE) { if (!img_cfp) cfclose(cf); mprintf(("ktx: read header error, unsupported texture format on file: file='%s'\n", filename)); return KTX1_ERROR_INVALID_FORMAT; } // skip metadata cfseek(cf, (int)hdr.bytesOfKeyValueData, CF_SEEK_CUR); // calculate max size uint32_t W = hdr.pixelWidth, H = hdr.pixelHeight; const uint32_t mips = std::max(1u, hdr.numberOfMipmapLevels); size_t total = 0; for (uint32_t level = 0; level < mips; ++level) { uint32_t imageSize = 0; cfread(&imageSize, 1, sizeof(imageSize), cf); total += imageSize; cfseek(cf, (int)imageSize, CF_SEEK_CUR); uint32_t pad = ((-(int)imageSize) & 3); if (pad) cfseek(cf, (int)pad, CF_SEEK_CUR); W = std::max(1u, W >> 1); H = std::max(1u, H >> 1); } if (w) *w = (int)hdr.pixelWidth; if (h) *h = (int)hdr.pixelHeight; if (c_type) *c_type = bm_ct; if (bpp) { switch (bm_ct) { case BM_TYPE_ETC2_RGB: case BM_TYPE_ETC2_SRGB: case BM_TYPE_ETC2_RGBA1: case BM_TYPE_ETC2_SRGBA1: *bpp = 24; break; case BM_TYPE_ETC2_RGBA_EAC: case BM_TYPE_ETC2_SRGBA_EAC: *bpp = 32; break; default: *bpp = 0; break; } } if (mip_levels) *mip_levels = (int)mips; if (total_size) *total_size = total; if (!img_cfp) cfclose(cf); return KTX1_ERROR_NONE; } int ktx1_read_bitmap(const char* filename, ubyte* dst, ubyte* out_bpp) { CFILE* cf; char real_name[MAX_FILENAME_LEN]; // copied from ddsutils // this better not happen.. ever Assert(filename != nullptr); // make sure there is an extension strcpy_s(real_name, filename); char* p = strchr(real_name, '.'); if (p) { *p = 0; } strcat_s(real_name, ".ktx"); // try to open the file cf = cfopen(real_name, "rb"); // file not found if (cf == nullptr) return KTX1_ERROR_INVALID_FILENAME; KTX1_Header hdr{}; int rc = KTX1_ERROR_NONE; if (cfread(&hdr, 1, sizeof(hdr), cf) != sizeof(hdr) || memcmp(hdr.identifier, KTX_ID, 12) != 0) { rc = KTX1_ERROR_BAD_HEADER; } if (hdr.glType != 0 || hdr.glFormat != 0) { rc = KTX1_ERROR_INVALID_FORMAT; } if (rc == KTX1_ERROR_NONE) { cfseek(cf, (int)hdr.bytesOfKeyValueData, CF_SEEK_CUR); const uint32_t mips = std::max(1u, hdr.numberOfMipmapLevels); size_t offset = 0; for (uint32_t level = 0; level < mips; ++level) { uint32_t image_size = 0; cfread(&image_size, 1, sizeof(image_size), cf); cfread(dst + offset, 1, image_size, cf); offset += image_size; uint32_t pad = ((-(int)image_size) & 3); // 4 byte padding if (pad) cfseek(cf, (int)pad, CF_SEEK_CUR); } if (out_bpp) *out_bpp = 0; } cfclose(cf); return rc; }