diff --git a/code/ddsutils/ddsutils.cpp b/code/ddsutils/ddsutils.cpp index 3f42e532dcc..4a85a1216e6 100644 --- a/code/ddsutils/ddsutils.cpp +++ b/code/ddsutils/ddsutils.cpp @@ -549,6 +549,100 @@ int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) return DDS_ERROR_NONE; } +int dds_decompress_top_mip_bgra(const char *filename, int cf_type, + int *out_width, int *out_height, + SCP_vector &out_pixels) +{ + Assert(filename != nullptr); + + // normalize to a .dds extension, same as dds_read_bitmap + char real_name[MAX_FILENAME_LEN]; + strcpy_s(real_name, filename); + char *p = strchr(real_name, '.'); + if (p) { *p = 0; } + strcat_s(real_name, ".dds"); + + CFILE *cfp = cfopen(real_name, "rb", cf_type); + if (cfp == nullptr) + return DDS_ERROR_INVALID_FILENAME; + + DDS_HEADER dds_header; + DDS_HEADER_DXT10 dx10_header; + int retval = _dds_read_header(cfp, dds_header, &dx10_header); + if (retval != DDS_ERROR_NONE) { + cfclose(cfp); + return retval; + } + + // only 2D FOURCC-compressed images are supported here + if (!(dds_header.ddspf.dwFlags & DDPF_FOURCC) || + (dds_header.dwCaps2 & DDSCAPS2_CUBEMAP)) { + cfclose(cfp); + return DDS_ERROR_UNSUPPORTED; + } + + void (*decode)(const void *, void *, int) = nullptr; + int block_size = 0; + switch (dds_header.ddspf.dwFourCC) { + case FOURCC_DXT1: decode = bcdec_bc1; block_size = BCDEC_BC1_BLOCK_SIZE; break; + case FOURCC_DXT3: decode = bcdec_bc2; block_size = BCDEC_BC2_BLOCK_SIZE; break; + case FOURCC_DXT5: decode = bcdec_bc3; block_size = BCDEC_BC3_BLOCK_SIZE; break; + case FOURCC_DX10: + if (!valid_dx10_format(dx10_header)) { + cfclose(cfp); + return DDS_ERROR_UNSUPPORTED; + } + decode = bcdec_bc7; + block_size = BCDEC_BC7_BLOCK_SIZE; + break; + default: + cfclose(cfp); + return DDS_ERROR_UNSUPPORTED; + } + + const int w = static_cast(dds_header.dwWidth); + const int h = static_cast(dds_header.dwHeight); + if (w <= 0 || h <= 0 || (w % 4) != 0 || (h % 4) != 0) { + cfclose(cfp); + return DDS_ERROR_INVALID_FORMAT; + } + + // _dds_read_header leaves the file positioned right after the header + // (including the DX10 sub-header if present), so the next read is the + // top mip's pixel data. + const int blocks_w = w / 4; + const int blocks_h = h / 4; + const size_t compressed_size = static_cast(blocks_w) * blocks_h * block_size; + + SCP_vector compressed(compressed_size); + const int got = cfread(compressed.data(), 1, static_cast(compressed_size), cfp); + cfclose(cfp); + if (got != static_cast(compressed_size)) + return DDS_ERROR_INVALID_FORMAT; + + out_pixels.assign(static_cast(w) * h * 4, 0); + ubyte *const dst = out_pixels.data(); + const ubyte *src = compressed.data(); + const int dst_stride = w * 4; + + for (int by = 0; by < blocks_h; ++by) { + for (int bx = 0; bx < blocks_w; ++bx) { + ubyte *blk_dst = dst + (by * 4) * dst_stride + (bx * 4) * 4; + decode(src, blk_dst, dst_stride); + src += block_size; + } + } + + // bcdec outputs RGBA byte-order; swap to BGRA + for (size_t x = 0; x < out_pixels.size(); x += 4) { + std::swap(out_pixels[x], out_pixels[x + 2]); + } + + if (out_width) *out_width = w; + if (out_height) *out_height = h; + 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) diff --git a/code/ddsutils/ddsutils.h b/code/ddsutils/ddsutils.h index 0d6a6ca6392..03c3694ce87 100644 --- a/code/ddsutils/ddsutils.h +++ b/code/ddsutils/ddsutils.h @@ -285,6 +285,15 @@ int dds_read_header(const char *filename, CFILE *img_cfp = NULL, int *width = 0, //size of the data it stored in size int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp = NULL, int cf_type = CF_TYPE_ANY); +// Decompress just the top mip of a 2D FOURCC-compressed DDS (DXT1/3/5, BC7) +// to 32-bpp BGRA, regardless of what the renderer's compression support is. +// Intended for tool/preview code that needs raw pixels and doesn't care +// about mipmaps or cubemap faces. On success, out_pixels is sized to +// width*height*4 in BGRA byte order. +int dds_decompress_top_mip_bgra(const char *filename, int cf_type, + int *out_width, int *out_height, + SCP_vector &out_pixels); + // writes a DDS file using given data void dds_save_image(int width, int height, int bpp, int num_mipmaps, ubyte *data = NULL, int cubemap = 0, const char *filename = NULL); diff --git a/qtfred/src/ui/util/ImageRenderer.cpp b/qtfred/src/ui/util/ImageRenderer.cpp index 772fc245e84..a253a821d23 100644 --- a/qtfred/src/ui/util/ImageRenderer.cpp +++ b/qtfred/src/ui/util/ImageRenderer.cpp @@ -1,6 +1,7 @@ #include "ImageRenderer.h" #include // bm_load, bm_get_info, bm_lock, bm_unlock +#include #include @@ -12,6 +13,24 @@ static void setError(QString* outError, const QString& text) *outError = text; } +// bm_lock_dds keeps compressed data as-is when the renderer reports s3tc/BPTC +// support, which would crash the regular 32-bpp QImage path. For the picker +// preview, ask ddsutils to decompress the top mip directly. +static bool decompressDdsToQImage(const char* bm_filename, QImage& outImage, QString* outError) +{ + int w = 0, h = 0; + SCP_vector pixels; + const int err = dds_decompress_top_mip_bgra(bm_filename, CF_TYPE_ANY, &w, &h, pixels); + if (err != DDS_ERROR_NONE) { + setError(outError, QStringLiteral("DDS decompress failed (%1).").arg(err)); + return false; + } + + QImage tmp(pixels.data(), w, h, w * 4, QImage::Format_ARGB32); + outImage = tmp.copy(); // detach before `pixels` goes out of scope + return !outImage.isNull(); +} + bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError) { outImage = QImage(); // clear @@ -21,6 +40,14 @@ bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError) return false; } + if (bm_is_compressed(bmHandle)) { + const char* fname = bm_get_filename(bmHandle); + if (fname && *fname) + return decompressDdsToQImage(fname, outImage, outError); + setError(outError, QStringLiteral("Compressed DDS with no filename; cannot preview.")); + return false; + } + int w = 0, h = 0; if (bm_get_info(bmHandle, &w, &h) < 0 || w <= 0 || h <= 0) { setError(outError, QStringLiteral("Bitmap has invalid info.")); @@ -35,8 +62,15 @@ bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError) return false; } - // rowsize is stored in pixels; multiply by bytes-per-pixel for the Qt stride. - const int bytesPerLine = bmp->w * (bmp->bpp >> 3); + // bm_lock_dds also doesn't honor the requested bpp for uncompressed DDS + // (e.g. 24-bpp RGB files), which would have us read past the buffer. + if (bmp->bpp != 32) { + bm_unlock(bmHandle); + setError(outError, QStringLiteral("Unsupported bitmap bpp (%1) for QImage preview.").arg(bmp->bpp)); + return false; + } + + const int bytesPerLine = bmp->w * 4; QImage tmp(reinterpret_cast(bmp->data), bmp->w, bmp->h, bytesPerLine, QImage::Format_ARGB32); outImage = tmp.copy(); // detach from bmpman memory before unlock bm_unlock(bmHandle); @@ -67,8 +101,9 @@ bool loadImageToQImage(const std::string& filename, QImage& outImage, QString* o const bool ok = loadHandleToQImage(handle, outImage, outError); - - // bm_unload(handle); TODO test unloading + // bm_unload is load_count aware, so if another + // part of qtfred is sharing the handle it stays alive for them. + bm_unload(handle); return ok; }