Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions code/ddsutils/ddsutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ubyte> &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<int>(dds_header.dwWidth);
const int h = static_cast<int>(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<size_t>(blocks_w) * blocks_h * block_size;

SCP_vector<ubyte> compressed(compressed_size);
const int got = cfread(compressed.data(), 1, static_cast<int>(compressed_size), cfp);
cfclose(cfp);
if (got != static_cast<int>(compressed_size))
return DDS_ERROR_INVALID_FORMAT;

out_pixels.assign(static_cast<size_t>(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)
Expand Down
9 changes: 9 additions & 0 deletions code/ddsutils/ddsutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<ubyte> &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);

Expand Down
43 changes: 39 additions & 4 deletions qtfred/src/ui/util/ImageRenderer.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "ImageRenderer.h"

#include <bmpman/bmpman.h> // bm_load, bm_get_info, bm_lock, bm_unlock
#include <ddsutils/ddsutils.h>

#include <QtGlobal>

Expand All @@ -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<ubyte> 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
Expand All @@ -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."));
Expand All @@ -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<const uchar*>(bmp->data), bmp->w, bmp->h, bytesPerLine, QImage::Format_ARGB32);
outImage = tmp.copy(); // detach from bmpman memory before unlock
bm_unlock(bmHandle);
Expand Down Expand Up @@ -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;
}
Expand Down
Loading