Files
fs2open.github.com/code/ktxutils/ktxutils.cpp
Shivansps 9f55626272 Fix incomplete implementation of ETC2 textures in Vulkan (#7699)
* finish etc2 textures in vulkan implementation

* add etcdec implementation

* Enable textureCompressionETC2 at vulkan device creation and fixed outdated comments
2026-11-03 12:20:11 +09:00

279 lines
7.1 KiB
C++

#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;
}