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
110 changes: 110 additions & 0 deletions code/ddsutils/ddsutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,116 @@ 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 | DDSCAPS2_VOLUME)) ||
(dds_header.dwFlags & DDSD_DEPTH) ||
(dds_header.dwDepth > 1)) {
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) ||
dx10_header.resourceDimension != D3D10_RESOURCE_DIMENSION::D3D10_RESOURCE_DIMENSION_TEXTURE2D ||
dx10_header.arraySize > 1) {
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) {
cfclose(cfp);
return DDS_ERROR_INVALID_FORMAT;
}

// BCn data is stored as ceil(w/4) * ceil(h/4) 4x4 blocks; dimensions
// don't have to be multiples of 4.
const int blocks_w = (w + 3) / 4;
const int blocks_h = (h + 3) / 4;
const int padded_w = blocks_w * 4;
const int padded_h = blocks_h * 4;
const size_t compressed_size = static_cast<size_t>(blocks_w) * blocks_h * block_size;

// _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.
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;

// Decode into a padded buffer so edge blocks have room, then crop to w*h.
SCP_vector<ubyte> decoded(static_cast<size_t>(padded_w) * padded_h * 4);
const int dec_stride = padded_w * 4;
const ubyte *src = compressed.data();

for (int by = 0; by < blocks_h; ++by) {
for (int bx = 0; bx < blocks_w; ++bx) {
ubyte *blk_dst = decoded.data() + (by * 4) * dec_stride + (bx * 4) * 4;
decode(src, blk_dst, dec_stride);
src += block_size;
}
}

out_pixels.assign(static_cast<size_t>(w) * h * 4, 0);
const int dst_stride = w * 4;
for (int y = 0; y < h; ++y) {
std::memcpy(out_pixels.data() + static_cast<size_t>(y) * dst_stride,
decoded.data() + static_cast<size_t>(y) * dec_stride,
static_cast<size_t>(dst_stride));
}

// 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
61 changes: 54 additions & 7 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 @@ -30,15 +57,34 @@ bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError)
// All FSO animation types (ANI, APNG, EFF) produce BGRA byte-order data
// at 32 bpp, which matches QImage::Format_ARGB32 on little-endian.
auto* bmp = bm_lock(bmHandle, 32, BMP_TEX_XPARENT);
if (bmp == nullptr || bmp->data == 0) {
if (bmp == nullptr) {
setError(outError, QStringLiteral("bm_lock failed."));
return false;
}
if (bmp->data == 0) {
// bm_lock incremented the refcount before populating data; release it.
bm_unlock(bmHandle);
setError(outError, QStringLiteral("bm_lock failed."));
return false;
}

// rowsize is stored in pixels; multiply by bytes-per-pixel for the Qt stride.
const int bytesPerLine = bmp->w * (bmp->bpp >> 3);
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_lock ignores the requested bpp for JPG (always 24, BGR) and for
// uncompressed DDS (whatever the file uses). Handle the two common
// cases (32-bpp BGRA and 24-bpp BGR) and reject anything else.
if (bmp->bpp == 32) {
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
} else if (bmp->bpp == 24) {
const int bytesPerLine = bmp->w * 3;
QImage tmp(reinterpret_cast<const uchar*>(bmp->data), bmp->w, bmp->h, bytesPerLine, QImage::Format_RGB888);
// FSO stores 24-bpp as BGR; swap to RGB and promote to ARGB32 (also detaches).
outImage = tmp.rgbSwapped().convertToFormat(QImage::Format_ARGB32);
} else {
bm_unlock(bmHandle);
setError(outError, QStringLiteral("Unsupported bitmap bpp (%1) for QImage preview.").arg(bmp->bpp));
return false;
}
bm_unlock(bmHandle);

if (outImage.isNull()) {
Expand Down Expand Up @@ -67,8 +113,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