Files
fs2open.github.com/code/ddsutils/ddsutils.cpp
T
Taylor Richards 9d3ba25606 fix various issues with DDS decompression (#7022)
Fix heap corruption with smaller mipmaps due to decompression output
always being a 4x4 block rather than actual mipmap size. It's necessary to
pad the allocated data size to allow for the overflow.

Fix heap corruption with DXT1 due to using improper data offsets for the type.

Fix data offsets not always taking the depth value into account.

Adjust code for slight increase in decode performance.

Update bcdec to 0.98
2025-09-08 21:49:48 -04:00

729 lines
19 KiB
C++

#include "ddsutils/ddsutils.h"
#include "cfile/cfile.h"
#include "osapi/osregistry.h"
#ifdef WITH_OPENGL
#include <glad/glad.h>
#else
static constexpr int GLAD_GL_EXT_texture_compression_s3tc = 0;
static constexpr int GLAD_GL_ARB_texture_compression_bptc = 0;
#endif
#define BCDEC_IMPLEMENTATION 1
PUSH_SUPPRESS_WARNINGS
#include "ddsutils/bcdec.h"
POP_SUPPRESS_WARNINGS
/* Currently supported formats:
* DXT1a (compressed)
* DXT1c (compressed)
* DXT3 (compressed)
* DXT5 (compressed)
* uncompressed 1555 (16-bit, 1-bit being alpha)
* uncompressed 8888 (32-bit)
* uncompressed 888 (24-bit, no alpha)
* uncompressed R8 (8-bit, Red channel as luminance)
*/
// power of two check, to verify that a given DDS will always be usable since
// some may not be power-of-2
static inline int is_power_of_two(int w, int h)
{
return ( (w && !(w & (w-1))) && (h && !(h & (h-1))) );
}
static bool valid_dx10_format(const DDS_HEADER_DXT10 &header)
{
switch (header.dxgiFormat) {
case DXGI_FORMAT::DXGI_FORMAT_BC7_TYPELESS:
case DXGI_FORMAT::DXGI_FORMAT_BC7_UNORM:
case DXGI_FORMAT::DXGI_FORMAT_BC7_UNORM_SRGB:
return true;
default:
return false;
}
}
static bool conversion_needed(const DDS_HEADER &dds_header)
{
if ((dds_header.ddspf.dwFlags & DDPF_FOURCC) != DDPF_FOURCC) {
return false;
}
switch (dds_header.ddspf.dwFourCC) {
case FOURCC_DXT1:
case FOURCC_DXT3:
case FOURCC_DXT5:
return !GLAD_GL_EXT_texture_compression_s3tc;
case FOURCC_DX10:
// anything other than BC7 will end up invalid
return !GLAD_GL_ARB_texture_compression_bptc;
default:
break;
}
// anything not specifically listed should end up invalid
return false;
}
// Memory usage for uncompressed textures is quite high. Some MVP assets can
// require well over a GB of VRAM for a single ship after conversion. To help
// alleviate this we need to resize those textures where possible. At 1024x1024
// the memory requirement is reduced to about 36 MB per ship with a full set
// of textures.
//
// NOTE: modifies header!!
//
// returns: number of mipmap levels to skip
static uint conversion_resize(DDS_HEADER &dds_header)
{
const size_t MAX_SIZE = 1024;
uint width, height, depth, offset = 0;
if (dds_header.dwMipMapCount <= 1) {
return 0;
}
width = dds_header.dwWidth;
height = dds_header.dwHeight;
depth = dds_header.dwDepth;
// drop levels until we get to an appropriate size, but make sure we have
// at least 1 mipmap level remaining at the end (in case there's not a full chain)
while (((width > MAX_SIZE) || (height > MAX_SIZE)) && (offset < dds_header.dwMipMapCount-1)) {
// this shouldn't happen, but catch the obscure case (like 8192x4)
if ((width <= 4) || (height <= 4)) {
break;
}
width >>= 1;
height >>= 1;
depth >>= 1;
++offset;
}
// no change needed
if ( !offset ) {
return offset;
}
// update header with new values
dds_header.dwWidth = std::max(1U, width);
dds_header.dwHeight = std::max(1U, height);
dds_header.dwDepth = std::max(1U, depth);
return offset;
}
static int _dds_read_header(CFILE *ddsfile, DDS_HEADER &dds_header, DDS_HEADER_DXT10 *dds_header_dx10 = nullptr)
{
DDS_HEADER_DXT10 dx10_header;
// read the code
int code = cfread_int(ddsfile);
// check it
if (code != DDS_FILECODE) {
return DDS_ERROR_BAD_HEADER;
}
// read header variables
dds_header.dwSize = cfread_uint(ddsfile);
dds_header.dwFlags = cfread_uint(ddsfile);
dds_header.dwHeight = cfread_uint(ddsfile);
dds_header.dwWidth = cfread_uint(ddsfile);
dds_header.dwPitchOrLinearSize = cfread_uint(ddsfile);
dds_header.dwDepth = cfread_uint(ddsfile);
dds_header.dwMipMapCount = cfread_uint(ddsfile);
// skip over the crap we don't care about
cfseek(ddsfile, 11*4, CF_SEEK_CUR);
dds_header.ddspf.dwSize = cfread_uint(ddsfile);
dds_header.ddspf.dwFlags = cfread_uint(ddsfile);
dds_header.ddspf.dwFourCC = cfread_uint(ddsfile);
dds_header.ddspf.dwRGBBitCount = cfread_uint(ddsfile);
dds_header.ddspf.dwRBitMask = cfread_uint(ddsfile);
dds_header.ddspf.dwGBitMask = cfread_uint(ddsfile);
dds_header.ddspf.dwBBitMask = cfread_uint(ddsfile);
dds_header.ddspf.dwABitMask = cfread_uint(ddsfile);
dds_header.dwCaps = cfread_uint(ddsfile);
dds_header.dwCaps2 = cfread_uint(ddsfile);
// Unused but need them checked for later
dds_header.dwCaps3 = cfread_uint(ddsfile);
dds_header.dwCaps4 = cfread_uint(ddsfile);
dds_header.dwReserved2 = cfread_uint(ddsfile);
if (dds_header.ddspf.dwFourCC == FOURCC_DX10) {
dx10_header.dxgiFormat = static_cast<DXGI_FORMAT>(cfread_uint(ddsfile));
dx10_header.resourceDimension = static_cast<D3D10_RESOURCE_DIMENSION>(cfread_uint(ddsfile));
dx10_header.miscFlag = cfread_uint(ddsfile);
dx10_header.arraySize = cfread_uint(ddsfile);
dx10_header.miscFlags2 = cfread_uint(ddsfile);
if (dds_header_dx10) {
memcpy(dds_header_dx10, &dx10_header, sizeof(DDS_HEADER_DXT10));
}
}
// sanitize values
dds_header.dwDepth = std::max(1U, dds_header.dwDepth);
dds_header.dwMipMapCount = std::max(1U, dds_header.dwMipMapCount);
#ifndef NDEBUG
if (dds_header.dwDepth > 1) {
Assertion(dds_header.dwFlags & DDSD_DEPTH, "ERROR: Volume texture missing proper flags!!");
}
#endif
// set pitch manually (per MS docs)
dds_header.dwFlags &= ~DDSD_LINEARSIZE;
dds_header.dwFlags |= DDSD_PITCH;
if (dds_header.ddspf.dwFlags & DDPF_FOURCC) {
int block_size = (dds_header.ddspf.dwFourCC == FOURCC_DXT1) ? 8 : 16;
dds_header.dwPitchOrLinearSize = std::max(1U, (dds_header.dwWidth+3)/4) * block_size;
} else {
// this isn't technically correct, but it's only wrong for formats we don't support
dds_header.dwPitchOrLinearSize = (dds_header.dwWidth * dds_header.ddspf.dwRGBBitCount + 7) / 8;
}
return DDS_ERROR_NONE;
}
static size_t compute_dds_size(const DDS_HEADER &dds_header, bool converting = false)
{
const uint block_sz = 4;
uint d_width = 0, d_height = 0, d_depth = 0;
size_t d_size = 0;
for (uint i = 0; i < dds_header.dwMipMapCount; i++) {
d_width = std::max(1U, dds_header.dwWidth >> i);
d_height = std::max(1U, dds_header.dwHeight >> i);
d_depth = std::max(1U, dds_header.dwDepth >> i);
// When converting we need to pad a bit to compensate for the decompression
// size on smaller mipmap levels. We need to ensure there is always enough
// room to decode an entire 4x4 block in rgba space
if (converting) {
auto sz = std::min(d_width, d_height);
if (sz < block_sz) {
d_size += ((block_sz * block_sz) - (sz * sz)) * d_depth * 4;
}
}
if (dds_header.ddspf.dwFlags & DDPF_FOURCC) {
// size of data block (4x4)
d_size += ((d_width + 3) / 4) * ((d_height + 3) / 4) * d_depth * ((dds_header.ddspf.dwFourCC == FOURCC_DXT1) ? 8 : 16);
} else {
d_size += d_width * d_height * d_depth * (dds_header.ddspf.dwRGBBitCount / 8);
}
}
// assumes valid cubemap (should be checked elsewhere)
if (dds_header.dwCaps2 & DDSCAPS2_CUBEMAP) {
// the previously computed size is just per face, mult by 6 for full size
d_size *= 6;
}
Assertion(d_size > 0, "ERROR: DDS size computed to 0!!");
return d_size;
}
static int get_bit_count(const DDS_HEADER &dds_header)
{
if (dds_header.ddspf.dwFlags & DDPF_RGB) {
return dds_header.ddspf.dwRGBBitCount;
} else if (dds_header.ddspf.dwFlags & DDPF_FOURCC) {
if (dds_header.ddspf.dwFourCC == FOURCC_DXT1) {
return 24;
} else {
return 32;
}
}
return 0;
}
int dds_read_header(const char *filename, CFILE *img_cfp, int *width, int *height, int *bpp, int *compression_type, int *levels, size_t *size)
{
DDS_HEADER dds_header;
DDS_HEADER_DXT10 dds_header_dx10;
CFILE *ddsfile;
char real_name[MAX_FILENAME_LEN];
int retval = DDS_ERROR_NONE;
int ct = DDS_UNCOMPRESSED;
int is_cubemap = 0;
bool convert = false;
if (img_cfp == NULL) {
// this better not happen.. ever
Assert(filename != NULL);
// make sure there is an extension
strcpy_s(real_name, filename);
char *p = strchr(real_name, '.');
if (p) { *p=0; }
strcat_s(real_name, ".dds");
// try to open the file
ddsfile = cfopen(real_name, "rb");
// file not found
if (ddsfile == NULL)
return DDS_ERROR_INVALID_FILENAME;
} else {
ddsfile = img_cfp;
}
retval = _dds_read_header(ddsfile, dds_header, &dds_header_dx10);
if (retval != DDS_ERROR_NONE) {
goto Done;
}
// check for valid cubemap (we require all 6 faces)
if (dds_header.dwCaps2 & DDSCAPS2_CUBEMAP) {
if ( !(dds_header.dwCaps2 & DDSCAPS2_CUBEMAP_ALLFACES) ) {
retval = DDS_ERROR_CUBEMAP_FACES;
ct = DDS_DXT_INVALID;
goto Done;
}
// it's a valid cubemap, probably
is_cubemap = 1;
}
// determine and verify the type of data for internal use
if (dds_header.ddspf.dwFlags & DDPF_FOURCC) {
switch (dds_header.ddspf.dwFourCC) {
case FOURCC_DXT1:
ct = (is_cubemap) ? DDS_CUBEMAP_DXT1 : DDS_DXT1;
break;
case FOURCC_DXT3:
ct = (is_cubemap) ? DDS_CUBEMAP_DXT3 : DDS_DXT3;
break;
case FOURCC_DXT5:
ct = (is_cubemap) ? DDS_CUBEMAP_DXT5 : DDS_DXT5;
break;
// dxt2 and dxt4 aren't supported
case FOURCC_DXT2:
case FOURCC_DXT4:
retval = DDS_ERROR_UNSUPPORTED;
ct = DDS_DXT_INVALID;
goto Done;
break;
case FOURCC_DX10:
if (valid_dx10_format(dds_header_dx10)) {
// Currently only BC7 is supported
ct = DDS_BC7;
} else {
retval = DDS_ERROR_UNSUPPORTED;
ct = DDS_DXT_INVALID;
goto Done;
}
break;
// none of the above
default:
retval = DDS_ERROR_INVALID_FORMAT;
ct = DDS_DXT_INVALID;
goto Done;
break;
}
} else if (dds_header.ddspf.dwFlags & DDPF_RGB) {
ct = (is_cubemap) ? DDS_CUBEMAP_UNCOMPRESSED : DDS_UNCOMPRESSED;
} else {
// it's not a readable format
retval = DDS_ERROR_INVALID_FORMAT;
goto Done;
}
// maybe do conversion if format not supported
convert = conversion_needed(dds_header);
if (convert) {
// switch to uncompressed format and reset vars
dds_header.ddspf.dwFlags &= ~DDPF_FOURCC;
dds_header.ddspf.dwFlags |= DDPF_RGB;
dds_header.ddspf.dwRGBBitCount = 32;
// NOTE: modifies header and returns offset for mipmap count
dds_header.dwMipMapCount -= conversion_resize(dds_header);
dds_header.dwMipMapCount = std::max(1U, dds_header.dwMipMapCount);
ct = (is_cubemap) ? DDS_CUBEMAP_UNCOMPRESSED : DDS_UNCOMPRESSED;
}
// we don't support compressed, non-power-of-2 images
if ( (ct != DDS_UNCOMPRESSED) && (!is_power_of_two(dds_header.dwWidth, dds_header.dwHeight)) ) {
retval = DDS_ERROR_NON_POWER_OF_2;
goto Done;
}
// stuff important info
if (size)
*size = compute_dds_size(dds_header, convert);
if (bpp)
*bpp = get_bit_count(dds_header);
if (compression_type)
*compression_type = ct;
if (width)
*width = dds_header.dwWidth;
if (height)
*height = dds_header.dwHeight;
if (levels)
*levels = dds_header.dwMipMapCount;
Done:
if (img_cfp == NULL) {
// close file and return
cfclose(ddsfile);
ddsfile = NULL;
}
return retval;
}
static void (*decompress_dds)(const void *in, void *out, int pitch) = nullptr;
static uint32_t BLOCK_SIZE = 0;
//reads pixel info from a dds file
int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type)
{
int retval;
size_t size = 0;
CFILE *cfp;
char real_name[MAX_FILENAME_LEN];
DDS_HEADER dds_header;
// this better not happen.. ever
Assert(filename != NULL);
// make sure there is an extension
strcpy_s(real_name, filename);
char *p = strchr(real_name, '.');
if (p) { *p = 0; }
strcat_s(real_name, ".dds");
// open it up and go to the data section
cfp = cfopen(real_name, "rb", cf_type);
// just in case
if (cfp == nullptr)
return DDS_ERROR_INVALID_FILENAME;
// read the header -- if its at this stage, it should be legal.
retval = _dds_read_header(cfp, dds_header);
Assert(retval == DDS_ERROR_NONE);
// this really shouldn't be needed but better safe than sorry
if (retval != DDS_ERROR_NONE) {
cfclose(cfp);
return retval;
}
cfseek(cfp, (dds_header.ddspf.dwFourCC == FOURCC_DX10) ? DX10_OFFSET : DDS_OFFSET, CF_SEEK_SET);
size = compute_dds_size(dds_header); // don't add padding on this one!!
// read in the data
if ( !conversion_needed(dds_header) ) {
cfread(data, 1, (int)size, cfp);
} else {
// Compression format not supported, convert to BGRA
ubyte *comp_data = (ubyte*)vm_malloc(size);
cfread(comp_data, 1, (int)size, cfp);
ubyte *src = comp_data;
ubyte *dst = data;
uint d_width, d_height, d_depth;
size_t data_offset = 0;
// NOTE: this alters the width, height, and depth values in the header,
// so we have to jump through some hoops to get the proper values
const uint mipmap_offset = conversion_resize(dds_header);
const int num_faces = (dds_header.dwCaps2 & DDSCAPS2_CUBEMAP) ? 6 : 1;
const bool has_depth = (dds_header.dwFlags & DDSD_DEPTH) == DDSD_DEPTH;
switch (dds_header.ddspf.dwFourCC) {
case FOURCC_DX10:
decompress_dds = bcdec_bc7;
BLOCK_SIZE = BCDEC_BC7_BLOCK_SIZE;
break;
case FOURCC_DXT5:
decompress_dds = bcdec_bc3;
BLOCK_SIZE = BCDEC_BC3_BLOCK_SIZE;
break;
case FOURCC_DXT1:
decompress_dds = bcdec_bc1;
BLOCK_SIZE = BCDEC_BC1_BLOCK_SIZE;
break;
case FOURCC_DXT3:
decompress_dds = bcdec_bc2;
BLOCK_SIZE = BCDEC_BC2_BLOCK_SIZE;
break;
default:
Error(LOCATION, "Invalid FourCC (%d) for DDS decompression!", dds_header.ddspf.dwFourCC);
break;
}
for (int f = 0; f < num_faces; ++f) {
// if we resized then skip over all of that data (altering values to match pre-resize)
for (uint x = 0; x < mipmap_offset; ++x) {
d_width = std::max(1U, dds_header.dwWidth << (mipmap_offset - x));
d_height = std::max(1U, dds_header.dwHeight << (mipmap_offset - x));
d_depth = has_depth ? std::max(1U, dds_header.dwDepth << (mipmap_offset - x)) : 1U;
src += ((d_width + 3) / 4) * ((d_height + 3) / 4) * d_depth * BLOCK_SIZE;
}
for (uint m = mipmap_offset; m < dds_header.dwMipMapCount; ++m) {
// width/height/depth at post-resize mipmap sizes, so compensate here
d_width = std::max(1U, dds_header.dwWidth >> (m - mipmap_offset));
d_height = std::max(1U, dds_header.dwHeight >> (m - mipmap_offset));
d_depth = std::max(1U, dds_header.dwDepth >> (m - mipmap_offset));
for (uint d = 0; d < d_depth; ++d) {
auto depth_offset = d * d_width * d_height * 4;
for (uint i = 0; i < d_height; i += 4) {
for (uint j = 0; j < d_width; j += 4) {
dst = data + data_offset + depth_offset + ((i * d_width + j) * 4);
decompress_dds(src, dst, d_width * 4);
src += BLOCK_SIZE;
}
}
}
// we need it in bgra, XOR to the rescue
for (size_t x = data_offset; x < (data_offset + (d_width * d_height * d_depth * 4)); x += 4) {
data[x] ^= data[x+2], data[x+2] ^= data[x], data[x] ^= data[x+2];
}
// bump data offset to next layer
data_offset += (d_width * d_height * d_depth * 4);
}
}
vm_free(comp_data);
comp_data = nullptr;
// switch to uncompressed format and reset vars (needed below to get correct bit count)
dds_header.ddspf.dwFlags &= ~DDPF_FOURCC;
dds_header.ddspf.dwFlags |= DDPF_RGB;
dds_header.ddspf.dwRGBBitCount = 32;
}
if (bpp)
*bpp = (ubyte)get_bit_count(dds_header);
// we look done here
cfclose(cfp);
return DDS_ERROR_NONE;
}
// save some image data as a DDS image
// NOTE: we only support, uncompressed, 24-bit RGB and 32-bit RGBA images here!!
void dds_save_image(int width, int height, int bpp, int num_mipmaps, ubyte *data, int cubemap, const char *filename)
{
DDS_HEADER dds_header;
char real_filename[MAX_FILENAME_LEN];
if (data == NULL) {
Int3();
return;
}
// header size check
if (sizeof(DDS_HEADER) != 124) {
Int3();
return;
}
// not that that's out of the way we can get down to business, constructing a sane filename
memset( &real_filename, 0, sizeof(real_filename) );
int count = os_config_read_uint(NULL, "ImageExportNum", 0);
if (count > 999)
count = 0;
if (filename == NULL) {
if (cubemap) {
sprintf(real_filename, "cubemap%.3d.dds", count++);
} else {
sprintf(real_filename, "image%.3d.dds", count++);
}
os_config_write_uint(NULL, "ImageExportNum", count);
} else {
Assert( strlen(filename) < MAX_FILENAME_LEN-5 );
strcpy_s(real_filename, filename);
char *p = strchr(real_filename, '.');
if (p) { *p = 0; }
strcat_s(real_filename, ".dds");
}
CFILE *image = cfopen( real_filename, "wb", CF_TYPE_CACHE );
if (image == NULL) {
mprintf(("Unable to open DDS image for saving!!\n"));
return;
}
if (num_mipmaps < 1)
num_mipmaps = 1;
// we have a filename and file handle, so now lets create our DDS header...
memset( &dds_header, 0, sizeof(DDS_HEADER) );
uint flags = (DDSD_CAPS | DDSD_PITCH | DDSD_PIXELFORMAT | DDSD_WIDTH | DDSD_HEIGHT);
uint pixel_flags = DDPF_RGB;
uint caps1 = DDSCAPS_TEXTURE;
uint caps2 = 0;
if (bpp == 32) {
pixel_flags |= DDPF_ALPHAPIXELS;
}
if (num_mipmaps > 1) {
flags |= DDSD_MIPMAPCOUNT;
caps1 |= (DDSCAPS_COMPLEX | DDSCAPS_MIPMAP);
}
if (cubemap) {
caps1 |= DDSCAPS_COMPLEX;
caps2 |= (DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_ALLFACES);
}
dds_header.dwSize = 124;
dds_header.dwFlags = flags;
dds_header.dwHeight = height;
dds_header.dwWidth = width;
dds_header.dwPitchOrLinearSize = (width * (bpp >> 3));
dds_header.dwDepth = 0;
dds_header.dwMipMapCount = num_mipmaps;
dds_header.ddspf.dwSize = 32;
dds_header.ddspf.dwFlags = pixel_flags;
dds_header.ddspf.dwFourCC = 0;
dds_header.ddspf.dwRGBBitCount = bpp;
dds_header.ddspf.dwRBitMask = 0x00ff0000;
dds_header.ddspf.dwGBitMask = 0x0000ff00;
dds_header.ddspf.dwBBitMask = 0x000000ff;
dds_header.ddspf.dwABitMask = (bpp == 32) ? 0xff000000 : 0x00000000;
dds_header.dwCaps = caps1;
dds_header.dwCaps2 = caps2;
// now save it all...
uint dds_id = DDS_FILECODE;
cfwrite(&dds_id, 1, 4, image);
cfwrite(&dds_header, 1, sizeof(DDS_HEADER), image);
int faces = (cubemap) ? 6 : 1;
int f_width = width;
int f_height = height;
int f_size = 0, f_offset = 0;
// cubemaps are written:
// face 1
// all face 1 mipmaps
// face 2
// all face 2 mipmaps
// ... etc.
for (int i = 0; i < faces; i++) {
for (int j = 0; j < num_mipmaps; j++) {
f_size = (f_width * f_height * (bpp >> 3));
cfwrite(data + f_offset, 1, f_size, image);
// increase offset for next mipmap level
f_offset += f_size;
// half width and height for next mipmap level
f_width /= 2;
f_height /= 2;
if (f_width < 1)
f_width = 1;
if (f_height < 1)
f_height = 1;
}
// reset width and height to original values for the next face
f_width = width;
f_height = height;
}
// done! now lets go get something to eat...
cfclose(image);
}
// returns string representation of DDS_** error code
const char *dds_error_string(int code)
{
switch (code)
{
case DDS_ERROR_NONE:
return "No error";
case DDS_ERROR_INVALID_FILENAME:
return "File not found";
case DDS_ERROR_BAD_HEADER:
return "Filecode did not equal \"DDS \"";
case DDS_ERROR_INVALID_FORMAT:
return "DDS was in an unsupported/unknown format";
case DDS_ERROR_UNSUPPORTED:
return "DDS format was known but is not supported (ie. DXT2/DXT4)";
case DDS_ERROR_NO_COMPRESSION:
return "DDS is compressed but compression support is not enabled";
case DDS_ERROR_NON_POWER_OF_2:
return "Cannot load DDS if not power-of-2";
case DDS_ERROR_CUBEMAP_FACES:
return "Cubemaps must have all 6 faces";
default:
return "Abort, retry, fail?";
}
}