aboutsummaryrefslogtreecommitdiff
path: root/libs/preview_dds_native/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/preview_dds_native/src')
-rw-r--r--libs/preview_dds_native/src/CMakeLists.txt17
-rw-r--r--libs/preview_dds_native/src/ddsfile.cpp132
-rw-r--r--libs/preview_dds_native/src/ddsfile.h47
-rw-r--r--libs/preview_dds_native/src/ddsformat.cpp619
-rw-r--r--libs/preview_dds_native/src/ddsformat.h200
-rw-r--r--libs/preview_dds_native/src/ddspreview.cpp126
-rw-r--r--libs/preview_dds_native/src/ddspreview.h44
-rw-r--r--libs/preview_dds_native/src/ddswidget.cpp252
-rw-r--r--libs/preview_dds_native/src/ddswidget.h36
9 files changed, 1473 insertions, 0 deletions
diff --git a/libs/preview_dds_native/src/CMakeLists.txt b/libs/preview_dds_native/src/CMakeLists.txt
new file mode 100644
index 0000000..c920d49
--- /dev/null
+++ b/libs/preview_dds_native/src/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 REQUIRED COMPONENTS OpenGL OpenGLWidgets)
+
+file(GLOB preview_dds_native_SOURCES CONFIGURE_DEPENDS
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.h
+)
+
+add_library(preview_dds_native SHARED ${preview_dds_native_SOURCES})
+mo2_configure_plugin(preview_dds_native NO_SOURCES WARNINGS OFF)
+target_link_libraries(preview_dds_native PRIVATE
+ mo2::uibase
+ Qt6::OpenGL
+ Qt6::OpenGLWidgets
+)
+mo2_install_plugin(preview_dds_native)
diff --git a/libs/preview_dds_native/src/ddsfile.cpp b/libs/preview_dds_native/src/ddsfile.cpp
new file mode 100644
index 0000000..91dc627
--- /dev/null
+++ b/libs/preview_dds_native/src/ddsfile.cpp
@@ -0,0 +1,132 @@
+#include "ddsfile.h"
+
+#include <QFile>
+
+#include <algorithm>
+#include <cstring>
+
+bool DDSFile::loadFromFile(const QString& path)
+{
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly)) {
+ return false;
+ }
+ return parse(file.readAll());
+}
+
+bool DDSFile::loadFromData(const QByteArray& data)
+{
+ return parse(data);
+}
+
+bool DDSFile::parse(const QByteArray& data)
+{
+ const char* ptr = data.constData();
+ int offset = 0;
+ int size = data.size();
+
+ // Magic number
+ if (size < 4)
+ return false;
+ uint32_t magic;
+ std::memcpy(&magic, ptr + offset, 4);
+ offset += 4;
+ if (magic != DDS_MAGIC)
+ return false;
+
+ // DDS header
+ if (size - offset < static_cast<int>(sizeof(DDSHeader)))
+ return false;
+ DDSHeader header;
+ std::memcpy(&header, ptr + offset, sizeof(DDSHeader));
+ offset += sizeof(DDSHeader);
+
+ if (header.dwSize != 124)
+ return false;
+
+ // DXT10 extended header
+ DDSHeaderDXT10 dxt10{};
+ bool hasDXT10 = false;
+ if ((header.ddspf.dwFlags & DDPF_FOURCC) &&
+ header.ddspf.dwFourCC == makeFourCC('D', 'X', '1', '0')) {
+ if (size - offset < static_cast<int>(sizeof(DDSHeaderDXT10)))
+ return false;
+ std::memcpy(&dxt10, ptr + offset, sizeof(DDSHeaderDXT10));
+ offset += sizeof(DDSHeaderDXT10);
+ hasDXT10 = true;
+ }
+
+ m_width = header.dwWidth;
+ m_height = header.dwHeight;
+
+ // Determine GL format
+ m_glFormat = getGLFormat(header.ddspf, hasDXT10 ? &dxt10 : nullptr);
+ if (!m_glFormat.valid) {
+ return false;
+ }
+
+ // Determine DXGI format for size calculations
+ if (hasDXT10) {
+ m_dxgiFormat = dxt10.dxgiFormat;
+ } else if (header.ddspf.dwFlags & DDPF_FOURCC) {
+ m_dxgiFormat = fourCCToDXGI(header.ddspf.dwFourCC);
+ } else {
+ m_dxgiFormat = DXGIFormat::UNKNOWN;
+ }
+
+ // Description
+ m_description = formatDescription(header.ddspf, hasDXT10 ? &dxt10 : nullptr);
+ m_description +=
+ QString(" | %1x%2").arg(m_width).arg(m_height);
+
+ // Cubemap detection
+ m_cubemap = false;
+ int layers = 1;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP) {
+ m_cubemap = true;
+ layers = 0;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_POSITIVEX) layers++;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_NEGATIVEX) layers++;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_POSITIVEY) layers++;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_NEGATIVEY) layers++;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_POSITIVEZ) layers++;
+ if (header.dwCaps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ) layers++;
+ m_description += " | Cubemap";
+ } else {
+ m_description += " | 2D";
+ }
+
+ // Mipmap count
+ int mipCount = 1;
+ if (header.dwFlags & DDSD_MIPMAPCOUNT) {
+ mipCount = std::max(1u, header.dwMipMapCount);
+ }
+ if (mipCount > 1) {
+ m_description += QString(" | %1 mips").arg(mipCount);
+ }
+
+ // Read pixel data for each face and mip level
+ m_faces.resize(layers);
+ for (int face = 0; face < layers; ++face) {
+ m_faces[face].mips.resize(mipCount);
+ uint32_t w = m_width;
+ uint32_t h = m_height;
+ for (int mip = 0; mip < mipCount; ++mip) {
+ uint32_t dataSize = mipDataSize(m_dxgiFormat, header.ddspf, w, h);
+ if (offset + static_cast<int>(dataSize) > size) {
+ // Truncated file, keep what we have
+ m_faces[face].mips.resize(mip);
+ break;
+ }
+ m_faces[face].mips[mip].width = w;
+ m_faces[face].mips[mip].height = h;
+ m_faces[face].mips[mip].data =
+ QByteArray(ptr + offset, static_cast<int>(dataSize));
+ offset += dataSize;
+ w = std::max(1u, w / 2);
+ h = std::max(1u, h / 2);
+ }
+ }
+
+ return !m_faces.isEmpty() && !m_faces[0].mips.isEmpty();
+}
diff --git a/libs/preview_dds_native/src/ddsfile.h b/libs/preview_dds_native/src/ddsfile.h
new file mode 100644
index 0000000..4242c61
--- /dev/null
+++ b/libs/preview_dds_native/src/ddsfile.h
@@ -0,0 +1,47 @@
+#ifndef DDSFILE_H
+#define DDSFILE_H
+
+#include "ddsformat.h"
+
+#include <QByteArray>
+#include <QString>
+#include <QVector>
+
+struct DDSMipLevel {
+ uint32_t width;
+ uint32_t height;
+ QByteArray data;
+};
+
+struct DDSFace {
+ QVector<DDSMipLevel> mips;
+};
+
+class DDSFile
+{
+public:
+ bool loadFromFile(const QString& path);
+ bool loadFromData(const QByteArray& data);
+
+ uint32_t width() const { return m_width; }
+ uint32_t height() const { return m_height; }
+ bool isCubemap() const { return m_cubemap; }
+ int faceCount() const { return m_faces.size(); }
+ int mipCount() const { return m_faces.isEmpty() ? 0 : m_faces[0].mips.size(); }
+ const DDSFace& face(int i) const { return m_faces[i]; }
+ const GLFormatInfo& glFormat() const { return m_glFormat; }
+ QString description() const { return m_description; }
+
+private:
+ bool parse(const QByteArray& data);
+
+ uint32_t m_width = 0;
+ uint32_t m_height = 0;
+ bool m_cubemap = false;
+ GLFormatInfo m_glFormat;
+ DXGIFormat m_dxgiFormat = DXGIFormat::UNKNOWN;
+ QString m_description;
+ QVector<DDSFace> m_faces;
+};
+
+#endif // DDSFILE_H
diff --git a/libs/preview_dds_native/src/ddsformat.cpp b/libs/preview_dds_native/src/ddsformat.cpp
new file mode 100644
index 0000000..b56db91
--- /dev/null
+++ b/libs/preview_dds_native/src/ddsformat.cpp
@@ -0,0 +1,619 @@
+#include "ddsformat.h"
+
+#include <QOpenGLFunctions>
+#include <QString>
+
+#include <algorithm>
+#include <cstring>
+
+// GL compressed format constants not always in headers
+#ifndef GL_COMPRESSED_RGB_S3TC_DXT1_EXT
+#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0
+#endif
+#ifndef GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
+#define GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 0x83F1
+#endif
+#ifndef GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
+#define GL_COMPRESSED_RGBA_S3TC_DXT3_EXT 0x83F2
+#endif
+#ifndef GL_COMPRESSED_RGBA_S3TC_DXT5_EXT
+#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3
+#endif
+#ifndef GL_COMPRESSED_RED_RGTC1
+#define GL_COMPRESSED_RED_RGTC1 0x8DBB
+#endif
+#ifndef GL_COMPRESSED_SIGNED_RED_RGTC1
+#define GL_COMPRESSED_SIGNED_RED_RGTC1 0x8DBC
+#endif
+#ifndef GL_COMPRESSED_RG_RGTC2
+#define GL_COMPRESSED_RG_RGTC2 0x8DBD
+#endif
+#ifndef GL_COMPRESSED_SIGNED_RG_RGTC2
+#define GL_COMPRESSED_SIGNED_RG_RGTC2 0x8DBE
+#endif
+#ifndef GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT
+#define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT 0x8E8F
+#endif
+#ifndef GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT
+#define GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT 0x8E8E
+#endif
+#ifndef GL_COMPRESSED_RGBA_BPTC_UNORM
+#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C
+#endif
+#ifndef GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM
+#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D
+#endif
+#ifndef GL_COMPRESSED_SRGB_S3TC_DXT1_EXT
+#define GL_COMPRESSED_SRGB_S3TC_DXT1_EXT 0x8C4C
+#endif
+#ifndef GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
+#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT 0x8C4D
+#endif
+#ifndef GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
+#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT 0x8C4E
+#endif
+#ifndef GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT
+#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT 0x8C4F
+#endif
+
+static GLFormatInfo dxgiToGL(DXGIFormat fmt)
+{
+ GLFormatInfo info;
+ info.valid = true;
+
+ switch (fmt) {
+ // BC1 (DXT1)
+ case DXGIFormat::BC1_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
+ break;
+ case DXGIFormat::BC1_UNORM_SRGB:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
+ break;
+ // BC2 (DXT3)
+ case DXGIFormat::BC2_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ break;
+ case DXGIFormat::BC2_UNORM_SRGB:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
+ break;
+ // BC3 (DXT5)
+ case DXGIFormat::BC3_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+ case DXGIFormat::BC3_UNORM_SRGB:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
+ break;
+ // BC4
+ case DXGIFormat::BC4_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RED_RGTC1;
+ break;
+ case DXGIFormat::BC4_SNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SIGNED_RED_RGTC1;
+ break;
+ // BC5
+ case DXGIFormat::BC5_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RG_RGTC2;
+ break;
+ case DXGIFormat::BC5_SNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SIGNED_RG_RGTC2;
+ break;
+ // BC6H
+ case DXGIFormat::BC6H_UF16:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT;
+ break;
+ case DXGIFormat::BC6H_SF16:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT;
+ break;
+ // BC7
+ case DXGIFormat::BC7_UNORM:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_RGBA_BPTC_UNORM;
+ break;
+ case DXGIFormat::BC7_UNORM_SRGB:
+ info.compressed = true;
+ info.internalFormat = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM;
+ break;
+
+ // Uncompressed RGBA
+ case DXGIFormat::R8G8B8A8_UNORM:
+ case DXGIFormat::R8G8B8A8_UNORM_SRGB:
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_RGBA;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+ case DXGIFormat::R8G8B8A8_UINT:
+ info.internalFormat = GL_RGBA8UI;
+ info.format = GL_RGBA_INTEGER;
+ info.type = GL_UNSIGNED_BYTE;
+ info.sampler = SamplerType::UInt;
+ break;
+ case DXGIFormat::R8G8B8A8_SNORM:
+ info.internalFormat = GL_RGBA8_SNORM;
+ info.format = GL_RGBA;
+ info.type = GL_BYTE;
+ break;
+ case DXGIFormat::R8G8B8A8_SINT:
+ info.internalFormat = GL_RGBA8I;
+ info.format = GL_RGBA_INTEGER;
+ info.type = GL_BYTE;
+ info.sampler = SamplerType::SInt;
+ break;
+
+ // BGRA
+ case DXGIFormat::B8G8R8A8_UNORM:
+ case DXGIFormat::B8G8R8A8_UNORM_SRGB:
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_BGRA;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+ case DXGIFormat::B8G8R8X8_UNORM:
+ case DXGIFormat::B8G8R8X8_UNORM_SRGB:
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_BGRA;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+
+ // 16-bit float
+ case DXGIFormat::R16G16B16A16_FLOAT:
+ info.internalFormat = GL_RGBA16F;
+ info.format = GL_RGBA;
+ info.type = GL_HALF_FLOAT;
+ break;
+ case DXGIFormat::R16G16B16A16_UNORM:
+ info.internalFormat = GL_RGBA16;
+ info.format = GL_RGBA;
+ info.type = GL_UNSIGNED_SHORT;
+ break;
+ case DXGIFormat::R16G16B16A16_UINT:
+ info.internalFormat = GL_RGBA16UI;
+ info.format = GL_RGBA_INTEGER;
+ info.type = GL_UNSIGNED_SHORT;
+ info.sampler = SamplerType::UInt;
+ break;
+ case DXGIFormat::R16G16B16A16_SNORM:
+ info.internalFormat = GL_RGBA16_SNORM;
+ info.format = GL_RGBA;
+ info.type = GL_SHORT;
+ break;
+ case DXGIFormat::R16G16B16A16_SINT:
+ info.internalFormat = GL_RGBA16I;
+ info.format = GL_RGBA_INTEGER;
+ info.type = GL_SHORT;
+ info.sampler = SamplerType::SInt;
+ break;
+
+ // 32-bit float
+ case DXGIFormat::R32G32B32A32_FLOAT:
+ info.internalFormat = GL_RGBA32F;
+ info.format = GL_RGBA;
+ info.type = GL_FLOAT;
+ break;
+ case DXGIFormat::R32G32B32_FLOAT:
+ info.internalFormat = GL_RGB32F;
+ info.format = GL_RGB;
+ info.type = GL_FLOAT;
+ break;
+
+ // RG formats
+ case DXGIFormat::R8G8_UNORM:
+ info.internalFormat = GL_RG8;
+ info.format = GL_RG;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+ case DXGIFormat::R16G16_FLOAT:
+ info.internalFormat = GL_RG16F;
+ info.format = GL_RG;
+ info.type = GL_HALF_FLOAT;
+ break;
+ case DXGIFormat::R16G16_UNORM:
+ info.internalFormat = GL_RG16;
+ info.format = GL_RG;
+ info.type = GL_UNSIGNED_SHORT;
+ break;
+ case DXGIFormat::R32G32_FLOAT:
+ info.internalFormat = GL_RG32F;
+ info.format = GL_RG;
+ info.type = GL_FLOAT;
+ break;
+
+ // R formats
+ case DXGIFormat::R8_UNORM:
+ info.internalFormat = GL_R8;
+ info.format = GL_RED;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+ case DXGIFormat::R16_FLOAT:
+ info.internalFormat = GL_R16F;
+ info.format = GL_RED;
+ info.type = GL_HALF_FLOAT;
+ break;
+ case DXGIFormat::R16_UNORM:
+ info.internalFormat = GL_R16;
+ info.format = GL_RED;
+ info.type = GL_UNSIGNED_SHORT;
+ break;
+ case DXGIFormat::R32_FLOAT:
+ info.internalFormat = GL_R32F;
+ info.format = GL_RED;
+ info.type = GL_FLOAT;
+ break;
+ case DXGIFormat::A8_UNORM:
+ info.internalFormat = GL_R8;
+ info.format = GL_RED;
+ info.type = GL_UNSIGNED_BYTE;
+ break;
+
+ // Packed formats
+ case DXGIFormat::R10G10B10A2_UNORM:
+ info.internalFormat = GL_RGB10_A2;
+ info.format = GL_RGBA;
+ info.type = GL_UNSIGNED_INT_2_10_10_10_REV;
+ break;
+ case DXGIFormat::R11G11B10_FLOAT:
+ info.internalFormat = GL_R11F_G11F_B10F;
+ info.format = GL_RGB;
+ info.type = GL_UNSIGNED_INT_10F_11F_11F_REV;
+ break;
+ case DXGIFormat::B5G6R5_UNORM:
+ info.internalFormat = GL_RGB565;
+ info.format = GL_RGB;
+ info.type = GL_UNSIGNED_SHORT_5_6_5;
+ break;
+ case DXGIFormat::B5G5R5A1_UNORM:
+ info.internalFormat = GL_RGB5_A1;
+ info.format = GL_BGRA;
+ info.type = GL_UNSIGNED_SHORT_1_5_5_5_REV;
+ break;
+ case DXGIFormat::B4G4R4A4_UNORM:
+ info.internalFormat = GL_RGBA4;
+ info.format = GL_BGRA;
+ info.type = GL_UNSIGNED_SHORT_4_4_4_4_REV;
+ break;
+
+ default:
+ info.valid = false;
+ break;
+ }
+
+ return info;
+}
+
+DXGIFormat fourCCToDXGI(uint32_t fourCC)
+{
+ if (fourCC == makeFourCC('D', 'X', 'T', '1'))
+ return DXGIFormat::BC1_UNORM;
+ if (fourCC == makeFourCC('D', 'X', 'T', '3'))
+ return DXGIFormat::BC2_UNORM;
+ if (fourCC == makeFourCC('D', 'X', 'T', '5'))
+ return DXGIFormat::BC3_UNORM;
+ if (fourCC == makeFourCC('B', 'C', '4', 'U') ||
+ fourCC == makeFourCC('A', 'T', 'I', '1'))
+ return DXGIFormat::BC4_UNORM;
+ if (fourCC == makeFourCC('B', 'C', '4', 'S'))
+ return DXGIFormat::BC4_SNORM;
+ if (fourCC == makeFourCC('A', 'T', 'I', '2') ||
+ fourCC == makeFourCC('B', 'C', '5', 'U'))
+ return DXGIFormat::BC5_UNORM;
+ if (fourCC == makeFourCC('B', 'C', '5', 'S'))
+ return DXGIFormat::BC5_SNORM;
+
+ // Numeric FourCC codes for float/half-float formats
+ switch (fourCC) {
+ case 36:
+ return DXGIFormat::R16G16B16A16_UNORM;
+ case 110:
+ return DXGIFormat::R16G16B16A16_SNORM;
+ case 111:
+ return DXGIFormat::R16_FLOAT;
+ case 112:
+ return DXGIFormat::R16G16_FLOAT;
+ case 113:
+ return DXGIFormat::R16G16B16A16_FLOAT;
+ case 114:
+ return DXGIFormat::R32_FLOAT;
+ case 115:
+ return DXGIFormat::R32G32_FLOAT;
+ case 116:
+ return DXGIFormat::R32G32B32A32_FLOAT;
+ default:
+ return DXGIFormat::UNKNOWN;
+ }
+}
+
+// Helper: count trailing zeros / bit shift for a mask
+static int maskShift(uint32_t mask)
+{
+ if (mask == 0)
+ return 0;
+ int shift = 0;
+ while ((mask & 1) == 0) {
+ mask >>= 1;
+ ++shift;
+ }
+ return shift;
+}
+
+static int maskBits(uint32_t mask)
+{
+ int count = 0;
+ while (mask) {
+ count += mask & 1;
+ mask >>= 1;
+ }
+ return count;
+}
+
+// Build a converter for arbitrary bitmask pixel formats
+static GLFormatInfo buildBitmaskFormat(const DDSPixelFormat& pf)
+{
+ GLFormatInfo info;
+ info.valid = true;
+
+ int rShift = maskShift(pf.dwRBitMask);
+ int gShift = maskShift(pf.dwGBitMask);
+ int bShift = maskShift(pf.dwBBitMask);
+ int aShift = maskShift(pf.dwABitMask);
+ int rBits = maskBits(pf.dwRBitMask);
+ int gBits = maskBits(pf.dwGBitMask);
+ int bBits = maskBits(pf.dwBBitMask);
+ int aBits = maskBits(pf.dwABitMask);
+ int bpp = pf.dwRGBBitCount;
+
+ // Try to match common uncompressed formats directly
+ if (bpp == 32 && pf.dwRBitMask == 0x000000FF && pf.dwGBitMask == 0x0000FF00 &&
+ pf.dwBBitMask == 0x00FF0000 &&
+ (pf.dwABitMask == 0xFF000000 || pf.dwABitMask == 0)) {
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_RGBA;
+ info.type = GL_UNSIGNED_BYTE;
+ return info;
+ }
+ if (bpp == 32 && pf.dwRBitMask == 0x00FF0000 && pf.dwGBitMask == 0x0000FF00 &&
+ pf.dwBBitMask == 0x000000FF) {
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_BGRA;
+ info.type = GL_UNSIGNED_BYTE;
+ return info;
+ }
+
+ // Generic converter: extract channels and pack into RGBA8
+ uint32_t rMask = pf.dwRBitMask;
+ uint32_t gMask = pf.dwGBitMask;
+ uint32_t bMask = pf.dwBBitMask;
+ uint32_t aMask = pf.dwABitMask;
+ bool hasAlpha = (pf.dwFlags & DDPF_ALPHAPIXELS) && aMask != 0;
+ int bytesPerPixel = bpp / 8;
+
+ info.internalFormat = GL_RGBA8;
+ info.format = GL_RGBA;
+ info.type = GL_UNSIGNED_BYTE;
+ info.converter = [=](const QByteArray& data, int w, int h) -> QByteArray {
+ QByteArray result(w * h * 4, '\0');
+ const uint8_t* src = reinterpret_cast<const uint8_t*>(data.constData());
+ uint8_t* dst = reinterpret_cast<uint8_t*>(result.data());
+
+ for (int i = 0; i < w * h; ++i) {
+ uint32_t pixel = 0;
+ std::memcpy(&pixel, src + i * bytesPerPixel,
+ std::min(bytesPerPixel, 4));
+
+ int r = rBits > 0 ? ((pixel & rMask) >> rShift) * 255 / ((1 << rBits) - 1) : 0;
+ int g = gBits > 0 ? ((pixel & gMask) >> gShift) * 255 / ((1 << gBits) - 1) : 0;
+ int b = bBits > 0 ? ((pixel & bMask) >> bShift) * 255 / ((1 << bBits) - 1) : 0;
+ int a = hasAlpha && aBits > 0
+ ? ((pixel & aMask) >> aShift) * 255 / ((1 << aBits) - 1)
+ : 255;
+
+ dst[i * 4 + 0] = static_cast<uint8_t>(r);
+ dst[i * 4 + 1] = static_cast<uint8_t>(g);
+ dst[i * 4 + 2] = static_cast<uint8_t>(b);
+ dst[i * 4 + 3] = static_cast<uint8_t>(a);
+ }
+ return result;
+ };
+
+ return info;
+}
+
+GLFormatInfo getGLFormat(const DDSPixelFormat& pf, const DDSHeaderDXT10* dxt10)
+{
+ // DX10 extended header takes priority
+ if (dxt10) {
+ return dxgiToGL(dxt10->dxgiFormat);
+ }
+
+ // FourCC compressed or float formats
+ if (pf.dwFlags & DDPF_FOURCC) {
+ DXGIFormat dxgi = fourCCToDXGI(pf.dwFourCC);
+ if (dxgi != DXGIFormat::UNKNOWN) {
+ return dxgiToGL(dxgi);
+ }
+ GLFormatInfo info;
+ info.valid = false;
+ return info;
+ }
+
+ // Uncompressed with bitmasks
+ if (pf.dwFlags & (DDPF_RGB | DDPF_LUMINANCE | DDPF_YUV | DDPF_ALPHA)) {
+ return buildBitmaskFormat(pf);
+ }
+
+ GLFormatInfo info;
+ info.valid = false;
+ return info;
+}
+
+static bool isBlockCompressed(DXGIFormat fmt)
+{
+ switch (fmt) {
+ case DXGIFormat::BC1_UNORM:
+ case DXGIFormat::BC1_UNORM_SRGB:
+ case DXGIFormat::BC2_UNORM:
+ case DXGIFormat::BC2_UNORM_SRGB:
+ case DXGIFormat::BC3_UNORM:
+ case DXGIFormat::BC3_UNORM_SRGB:
+ case DXGIFormat::BC4_UNORM:
+ case DXGIFormat::BC4_SNORM:
+ case DXGIFormat::BC5_UNORM:
+ case DXGIFormat::BC5_SNORM:
+ case DXGIFormat::BC6H_UF16:
+ case DXGIFormat::BC6H_SF16:
+ case DXGIFormat::BC7_UNORM:
+ case DXGIFormat::BC7_UNORM_SRGB:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int blockSize(DXGIFormat fmt)
+{
+ switch (fmt) {
+ case DXGIFormat::BC1_UNORM:
+ case DXGIFormat::BC1_UNORM_SRGB:
+ case DXGIFormat::BC4_UNORM:
+ case DXGIFormat::BC4_SNORM:
+ return 8;
+ default:
+ return 16;
+ }
+}
+
+uint32_t mipDataSize(DXGIFormat fmt, const DDSPixelFormat& pf,
+ uint32_t width, uint32_t height)
+{
+ if (isBlockCompressed(fmt)) {
+ uint32_t blocksW = std::max(1u, (width + 3) / 4);
+ uint32_t blocksH = std::max(1u, (height + 3) / 4);
+ return blocksW * blocksH * blockSize(fmt);
+ }
+
+ // Uncompressed: use bits per pixel
+ uint32_t bpp = pf.dwRGBBitCount;
+ if (bpp == 0) {
+ // Estimate from DXGI format for non-bitmask formats
+ switch (fmt) {
+ case DXGIFormat::R32G32B32A32_FLOAT:
+ bpp = 128;
+ break;
+ case DXGIFormat::R32G32B32_FLOAT:
+ bpp = 96;
+ break;
+ case DXGIFormat::R16G16B16A16_FLOAT:
+ case DXGIFormat::R16G16B16A16_UNORM:
+ case DXGIFormat::R16G16B16A16_SNORM:
+ case DXGIFormat::R32G32_FLOAT:
+ bpp = 64;
+ break;
+ case DXGIFormat::R8G8B8A8_UNORM:
+ case DXGIFormat::R8G8B8A8_UNORM_SRGB:
+ case DXGIFormat::B8G8R8A8_UNORM:
+ case DXGIFormat::B8G8R8X8_UNORM:
+ case DXGIFormat::R16G16_FLOAT:
+ case DXGIFormat::R16G16_UNORM:
+ case DXGIFormat::R32_FLOAT:
+ case DXGIFormat::R10G10B10A2_UNORM:
+ case DXGIFormat::R11G11B10_FLOAT:
+ bpp = 32;
+ break;
+ case DXGIFormat::R8G8_UNORM:
+ case DXGIFormat::R16_FLOAT:
+ case DXGIFormat::R16_UNORM:
+ case DXGIFormat::B5G6R5_UNORM:
+ case DXGIFormat::B5G5R5A1_UNORM:
+ case DXGIFormat::B4G4R4A4_UNORM:
+ bpp = 16;
+ break;
+ case DXGIFormat::R8_UNORM:
+ case DXGIFormat::A8_UNORM:
+ bpp = 8;
+ break;
+ default:
+ bpp = 32;
+ break;
+ }
+ }
+ return width * height * bpp / 8;
+}
+
+static const char* dxgiFormatName(DXGIFormat fmt)
+{
+ switch (fmt) {
+ case DXGIFormat::BC1_UNORM:
+ return "BC1_UNORM (DXT1)";
+ case DXGIFormat::BC1_UNORM_SRGB:
+ return "BC1_UNORM_SRGB";
+ case DXGIFormat::BC2_UNORM:
+ return "BC2_UNORM (DXT3)";
+ case DXGIFormat::BC2_UNORM_SRGB:
+ return "BC2_UNORM_SRGB";
+ case DXGIFormat::BC3_UNORM:
+ return "BC3_UNORM (DXT5)";
+ case DXGIFormat::BC3_UNORM_SRGB:
+ return "BC3_UNORM_SRGB";
+ case DXGIFormat::BC4_UNORM:
+ return "BC4_UNORM";
+ case DXGIFormat::BC4_SNORM:
+ return "BC4_SNORM";
+ case DXGIFormat::BC5_UNORM:
+ return "BC5_UNORM";
+ case DXGIFormat::BC5_SNORM:
+ return "BC5_SNORM";
+ case DXGIFormat::BC6H_UF16:
+ return "BC6H_UF16";
+ case DXGIFormat::BC6H_SF16:
+ return "BC6H_SF16";
+ case DXGIFormat::BC7_UNORM:
+ return "BC7_UNORM";
+ case DXGIFormat::BC7_UNORM_SRGB:
+ return "BC7_UNORM_SRGB";
+ case DXGIFormat::R8G8B8A8_UNORM:
+ return "R8G8B8A8_UNORM";
+ case DXGIFormat::R8G8B8A8_UNORM_SRGB:
+ return "R8G8B8A8_UNORM_SRGB";
+ case DXGIFormat::B8G8R8A8_UNORM:
+ return "B8G8R8A8_UNORM";
+ case DXGIFormat::R16G16B16A16_FLOAT:
+ return "R16G16B16A16_FLOAT";
+ case DXGIFormat::R32G32B32A32_FLOAT:
+ return "R32G32B32A32_FLOAT";
+ default:
+ return "Unknown";
+ }
+}
+
+QString formatDescription(const DDSPixelFormat& pf, const DDSHeaderDXT10* dxt10)
+{
+ if (dxt10) {
+ return QString("DXGI: %1").arg(dxgiFormatName(dxt10->dxgiFormat));
+ }
+
+ if (pf.dwFlags & DDPF_FOURCC) {
+ char cc[5] = {0};
+ std::memcpy(cc, &pf.dwFourCC, 4);
+ DXGIFormat dxgi = fourCCToDXGI(pf.dwFourCC);
+ if (dxgi != DXGIFormat::UNKNOWN) {
+ return QString("FourCC: %1 (%2)").arg(cc).arg(dxgiFormatName(dxgi));
+ }
+ return QString("FourCC: %1").arg(cc);
+ }
+
+ return QString("%1-bit R:0x%2 G:0x%3 B:0x%4 A:0x%5")
+ .arg(pf.dwRGBBitCount)
+ .arg(pf.dwRBitMask, 0, 16)
+ .arg(pf.dwGBitMask, 0, 16)
+ .arg(pf.dwBBitMask, 0, 16)
+ .arg(pf.dwABitMask, 0, 16);
+}
diff --git a/libs/preview_dds_native/src/ddsformat.h b/libs/preview_dds_native/src/ddsformat.h
new file mode 100644
index 0000000..22d3565
--- /dev/null
+++ b/libs/preview_dds_native/src/ddsformat.h
@@ -0,0 +1,200 @@
+#ifndef DDSFORMAT_H
+#define DDSFORMAT_H
+
+#include <cstdint>
+#include <functional>
+#include <vector>
+
+#include <QByteArray>
+
+// DDS file magic number
+constexpr uint32_t DDS_MAGIC = 0x20534444; // "DDS "
+
+// DDS_PIXELFORMAT flags
+constexpr uint32_t DDPF_ALPHAPIXELS = 0x1;
+constexpr uint32_t DDPF_ALPHA = 0x2;
+constexpr uint32_t DDPF_FOURCC = 0x4;
+constexpr uint32_t DDPF_RGB = 0x40;
+constexpr uint32_t DDPF_YUV = 0x200;
+constexpr uint32_t DDPF_LUMINANCE = 0x20000;
+
+// DDS_HEADER flags
+constexpr uint32_t DDSD_CAPS = 0x1;
+constexpr uint32_t DDSD_HEIGHT = 0x2;
+constexpr uint32_t DDSD_WIDTH = 0x4;
+constexpr uint32_t DDSD_PITCH = 0x8;
+constexpr uint32_t DDSD_PIXELFORMAT = 0x1000;
+constexpr uint32_t DDSD_MIPMAPCOUNT = 0x20000;
+constexpr uint32_t DDSD_LINEARSIZE = 0x80000;
+constexpr uint32_t DDSD_DEPTH = 0x800000;
+
+// DDS_HEADER caps2
+constexpr uint32_t DDSCAPS2_CUBEMAP = 0x200;
+constexpr uint32_t DDSCAPS2_CUBEMAP_POSITIVEX = 0x400;
+constexpr uint32_t DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800;
+constexpr uint32_t DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000;
+constexpr uint32_t DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000;
+constexpr uint32_t DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000;
+constexpr uint32_t DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000;
+constexpr uint32_t DDSCAPS2_VOLUME = 0x200000;
+
+constexpr uint32_t DDSCAPS2_CUBEMAP_ALLFACES =
+ DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX |
+ DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY |
+ DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ;
+
+// DXGI formats (subset covering common DDS textures)
+enum class DXGIFormat : uint32_t {
+ UNKNOWN = 0,
+ R32G32B32A32_FLOAT = 2,
+ R32G32B32A32_UINT = 3,
+ R32G32B32A32_SINT = 4,
+ R32G32B32_FLOAT = 6,
+ R32G32B32_UINT = 7,
+ R32G32B32_SINT = 8,
+ R16G16B16A16_FLOAT = 10,
+ R16G16B16A16_UNORM = 11,
+ R16G16B16A16_UINT = 12,
+ R16G16B16A16_SNORM = 13,
+ R16G16B16A16_SINT = 14,
+ R32G32_FLOAT = 16,
+ R32G32_UINT = 17,
+ R32G32_SINT = 18,
+ R10G10B10A2_UNORM = 24,
+ R10G10B10A2_UINT = 25,
+ R11G11B10_FLOAT = 26,
+ R8G8B8A8_UNORM = 28,
+ R8G8B8A8_UNORM_SRGB = 29,
+ R8G8B8A8_UINT = 30,
+ R8G8B8A8_SNORM = 31,
+ R8G8B8A8_SINT = 32,
+ R16G16_FLOAT = 34,
+ R16G16_UNORM = 35,
+ R16G16_UINT = 36,
+ R16G16_SNORM = 37,
+ R16G16_SINT = 38,
+ R32_FLOAT = 41,
+ R32_UINT = 42,
+ R32_SINT = 43,
+ R8G8_UNORM = 49,
+ R8G8_UINT = 50,
+ R8G8_SNORM = 51,
+ R8G8_SINT = 52,
+ R16_FLOAT = 54,
+ R16_UNORM = 56,
+ R16_UINT = 57,
+ R16_SNORM = 58,
+ R16_SINT = 59,
+ R8_UNORM = 61,
+ R8_UINT = 62,
+ R8_SNORM = 63,
+ R8_SINT = 64,
+ A8_UNORM = 65,
+ BC1_UNORM = 71,
+ BC1_UNORM_SRGB = 72,
+ BC2_UNORM = 74,
+ BC2_UNORM_SRGB = 75,
+ BC3_UNORM = 77,
+ BC3_UNORM_SRGB = 78,
+ BC4_UNORM = 80,
+ BC4_SNORM = 81,
+ BC5_UNORM = 83,
+ BC5_SNORM = 84,
+ B5G6R5_UNORM = 85,
+ B5G5R5A1_UNORM = 86,
+ B8G8R8A8_UNORM = 87,
+ B8G8R8X8_UNORM = 88,
+ B8G8R8A8_UNORM_SRGB = 91,
+ B8G8R8X8_UNORM_SRGB = 93,
+ BC6H_UF16 = 95,
+ BC6H_SF16 = 96,
+ BC7_UNORM = 98,
+ BC7_UNORM_SRGB = 99,
+ B4G4R4A4_UNORM = 115,
+};
+
+// D3D10 resource dimension
+enum class D3D10ResourceDimension : uint32_t {
+ Unknown = 0,
+ Buffer = 1,
+ Texture1D = 2,
+ Texture2D = 3,
+ Texture3D = 4,
+};
+
+#pragma pack(push, 1)
+
+struct DDSPixelFormat {
+ uint32_t dwSize;
+ uint32_t dwFlags;
+ uint32_t dwFourCC;
+ uint32_t dwRGBBitCount;
+ uint32_t dwRBitMask;
+ uint32_t dwGBitMask;
+ uint32_t dwBBitMask;
+ uint32_t dwABitMask;
+};
+
+struct DDSHeader {
+ uint32_t dwSize;
+ uint32_t dwFlags;
+ uint32_t dwHeight;
+ uint32_t dwWidth;
+ uint32_t dwPitchOrLinearSize;
+ uint32_t dwDepth;
+ uint32_t dwMipMapCount;
+ uint32_t dwReserved1[11];
+ DDSPixelFormat ddspf;
+ uint32_t dwCaps;
+ uint32_t dwCaps2;
+ uint32_t dwCaps3;
+ uint32_t dwCaps4;
+ uint32_t dwReserved2;
+};
+
+struct DDSHeaderDXT10 {
+ DXGIFormat dxgiFormat;
+ D3D10ResourceDimension resourceDimension;
+ uint32_t miscFlag;
+ uint32_t arraySize;
+ uint32_t miscFlags2;
+};
+
+#pragma pack(pop)
+
+// Sampler type for shader selection
+enum class SamplerType { Float, UInt, SInt };
+
+// OpenGL format info
+struct GLFormatInfo {
+ bool valid = false;
+ bool compressed = false;
+ uint32_t internalFormat = 0;
+ uint32_t format = 0; // only for uncompressed
+ uint32_t type = 0; // only for uncompressed
+ SamplerType sampler = SamplerType::Float;
+ // Optional converter for non-standard bitmask formats
+ std::function<QByteArray(const QByteArray&, int, int)> converter;
+};
+
+// Inline helper: make a FourCC from 4 chars
+constexpr uint32_t makeFourCC(char a, char b, char c, char d)
+{
+ return static_cast<uint32_t>(a) | (static_cast<uint32_t>(b) << 8) |
+ (static_cast<uint32_t>(c) << 16) | (static_cast<uint32_t>(d) << 24);
+}
+
+// Convert a FourCC code to DXGI format
+DXGIFormat fourCCToDXGI(uint32_t fourCC);
+
+// Resolve the OpenGL format from a DDS pixel format + optional DXT10 header
+GLFormatInfo getGLFormat(const DDSPixelFormat& pf, const DDSHeaderDXT10* dxt10);
+
+// Calculate mip level data size in bytes
+uint32_t mipDataSize(DXGIFormat fmt, const DDSPixelFormat& pf,
+ uint32_t width, uint32_t height);
+
+// Get a human-readable format description
+QString formatDescription(const DDSPixelFormat& pf, const DDSHeaderDXT10* dxt10);
+
+#endif // DDSFORMAT_H
diff --git a/libs/preview_dds_native/src/ddspreview.cpp b/libs/preview_dds_native/src/ddspreview.cpp
new file mode 100644
index 0000000..2bb95be
--- /dev/null
+++ b/libs/preview_dds_native/src/ddspreview.cpp
@@ -0,0 +1,126 @@
+#include "ddspreview.h"
+#include "ddsfile.h"
+#include "ddswidget.h"
+
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QWidget>
+
+using namespace MOBase;
+
+DDSPreview::DDSPreview() {}
+
+bool DDSPreview::init(IOrganizer* moInfo)
+{
+ m_organizer = moInfo;
+ return true;
+}
+
+QString DDSPreview::name() const
+{
+ return "DDS Preview (Native)";
+}
+
+QString DDSPreview::localizedName() const
+{
+ return tr("DDS Preview (Native)");
+}
+
+QString DDSPreview::author() const
+{
+ return "AnyOldName3";
+}
+
+QString DDSPreview::description() const
+{
+ return tr("Displays DDS texture files using OpenGL.");
+}
+
+VersionInfo DDSPreview::version() const
+{
+ return VersionInfo(1, 0, 0, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> DDSPreview::settings() const
+{
+ return {};
+}
+
+std::set<QString> DDSPreview::supportedExtensions() const
+{
+ return {"dds"};
+}
+
+bool DDSPreview::supportsArchives() const
+{
+ return true;
+}
+
+QWidget* DDSPreview::genFilePreview(const QString& fileName,
+ const QSize& maxSize) const
+{
+ DDSFile dds;
+ if (!dds.loadFromFile(fileName)) {
+ QLabel* label = new QLabel(tr("Failed to load DDS file."));
+ label->setAlignment(Qt::AlignCenter);
+ return label;
+ }
+ return buildPreview(dds, maxSize);
+}
+
+QWidget* DDSPreview::genDataPreview(const QByteArray& fileData,
+ const QString& fileName,
+ const QSize& maxSize) const
+{
+ DDSFile dds;
+ if (!dds.loadFromData(fileData)) {
+ QLabel* label = new QLabel(tr("Failed to load DDS data."));
+ label->setAlignment(Qt::AlignCenter);
+ return label;
+ }
+ return buildPreview(dds, maxSize);
+}
+
+QWidget* DDSPreview::buildPreview(DDSFile& dds, const QSize& maxSize) const
+{
+ QWidget* container = new QWidget();
+ QVBoxLayout* layout = new QVBoxLayout(container);
+
+ // Description label
+ QLabel* descLabel = new QLabel(dds.description());
+ descLabel->setAlignment(Qt::AlignCenter);
+ layout->addWidget(descLabel);
+
+ // OpenGL preview widget — we need to keep the DDSFile alive, so store
+ // it in the container. We use a shared_ptr stored as a property.
+ auto* ddsPtr = new DDSFile(std::move(dds));
+ DDSWidget* widget = new DDSWidget(*ddsPtr, container);
+
+ // Clean up DDSFile when container is destroyed
+ QObject::connect(container, &QObject::destroyed, [ddsPtr]() {
+ delete ddsPtr;
+ });
+
+ // Size the widget proportionally
+ int previewW = maxSize.width();
+ int previewH = maxSize.height() - 30; // leave room for label
+ if (ddsPtr->width() > 0 && ddsPtr->height() > 0) {
+ float texAspect =
+ static_cast<float>(ddsPtr->width()) / ddsPtr->height();
+ float availAspect =
+ static_cast<float>(previewW) / std::max(1, previewH);
+ if (texAspect > availAspect) {
+ previewH = static_cast<int>(previewW / texAspect);
+ } else {
+ previewW = static_cast<int>(previewH * texAspect);
+ }
+ }
+ widget->setMinimumSize(std::min(previewW, static_cast<int>(ddsPtr->width())),
+ std::min(previewH, static_cast<int>(ddsPtr->height())));
+ widget->setMaximumSize(previewW, previewH);
+
+ layout->addWidget(widget);
+ container->setLayout(layout);
+
+ return container;
+}
diff --git a/libs/preview_dds_native/src/ddspreview.h b/libs/preview_dds_native/src/ddspreview.h
new file mode 100644
index 0000000..36f2bec
--- /dev/null
+++ b/libs/preview_dds_native/src/ddspreview.h
@@ -0,0 +1,44 @@
+#ifndef DDSPREVIEW_H
+#define DDSPREVIEW_H
+
+#include <set>
+
+#include <QString>
+
+#include <uibase/iplugin.h>
+#include <uibase/ipluginpreview.h>
+
+class DDSPreview : public MOBase::IPluginPreview
+{
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview)
+ Q_PLUGIN_METADATA(IID "org.tannin.DDSPreviewNative")
+
+public:
+ DDSPreview();
+
+public: // IPlugin
+ bool init(MOBase::IOrganizer* moInfo) override;
+ QString name() const override;
+ QString localizedName() const override;
+ QString author() const override;
+ QString description() const override;
+ MOBase::VersionInfo version() const override;
+ QList<MOBase::PluginSetting> settings() const override;
+
+public: // IPluginPreview
+ std::set<QString> supportedExtensions() const override;
+ QWidget* genFilePreview(const QString& fileName,
+ const QSize& maxSize) const override;
+ bool supportsArchives() const override;
+ QWidget* genDataPreview(const QByteArray& fileData,
+ const QString& fileName,
+ const QSize& maxSize) const override;
+
+private:
+ QWidget* buildPreview(class DDSFile& dds, const QSize& maxSize) const;
+
+ MOBase::IOrganizer* m_organizer = nullptr;
+};
+
+#endif // DDSPREVIEW_H
diff --git a/libs/preview_dds_native/src/ddswidget.cpp b/libs/preview_dds_native/src/ddswidget.cpp
new file mode 100644
index 0000000..43eb586
--- /dev/null
+++ b/libs/preview_dds_native/src/ddswidget.cpp
@@ -0,0 +1,252 @@
+#include "ddswidget.h"
+
+#include <QDebug>
+
+#include <algorithm>
+#include <cstring>
+
+// Vertex data: position (x,y) + texcoord (u,v)
+static const float quadVertices[] = {
+ // pos // tex
+ -1.0f, -1.0f, 0.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, 0.0f, 0.0f,
+ 1.0f, 1.0f, 1.0f, 0.0f,
+};
+
+static const char* vertexShaderSrc = R"(
+#version 330 core
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aTexCoord;
+out vec2 vTexCoord;
+uniform float uAspect;
+uniform float uWidgetAspect;
+void main() {
+ vec2 pos = aPos;
+ float ratio = uAspect / uWidgetAspect;
+ if (ratio > 1.0)
+ pos.y *= 1.0 / ratio;
+ else
+ pos.x *= ratio;
+ gl_Position = vec4(pos, 0.0, 1.0);
+ vTexCoord = aTexCoord;
+}
+)";
+
+static const char* fragmentShaderSrc = R"(
+#version 330 core
+in vec2 vTexCoord;
+out vec4 fragColor;
+uniform sampler2D uTexture;
+void main() {
+ vec4 texel = texture(uTexture, vTexCoord);
+ // Checkerboard for transparency
+ vec2 checker = floor(vTexCoord * 16.0);
+ float c = mod(checker.x + checker.y, 2.0);
+ vec3 bg = mix(vec3(0.6), vec3(0.4), c);
+ fragColor = vec4(mix(bg, texel.rgb, texel.a), 1.0);
+}
+)";
+
+static const char* cubemapFragSrc = R"(
+#version 330 core
+in vec2 vTexCoord;
+out vec4 fragColor;
+uniform samplerCube uTexture;
+void main() {
+ // Spherical projection for cubemap preview
+ float theta = vTexCoord.x * 6.28318530718;
+ float phi = vTexCoord.y * 3.14159265359;
+ vec3 dir = vec3(sin(phi) * cos(theta), cos(phi), sin(phi) * sin(theta));
+ vec4 texel = texture(uTexture, dir);
+ vec2 checker = floor(vTexCoord * 16.0);
+ float c = mod(checker.x + checker.y, 2.0);
+ vec3 bg = mix(vec3(0.6), vec3(0.4), c);
+ fragColor = vec4(mix(bg, texel.rgb, texel.a), 1.0);
+}
+)";
+
+DDSWidget::DDSWidget(const DDSFile& dds, QWidget* parent)
+ : QOpenGLWidget(parent), m_dds(dds), m_vbo(QOpenGLBuffer::VertexBuffer)
+{
+ if (dds.height() > 0) {
+ m_aspectRatio =
+ static_cast<float>(dds.width()) / static_cast<float>(dds.height());
+ }
+}
+
+DDSWidget::~DDSWidget()
+{
+ makeCurrent();
+ delete m_texture;
+ delete m_shader;
+ m_vbo.destroy();
+ doneCurrent();
+}
+
+void DDSWidget::initializeGL()
+{
+ initializeOpenGLFunctions();
+
+ glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
+
+ // VBO
+ m_vbo.create();
+ m_vbo.bind();
+ m_vbo.allocate(quadVertices, sizeof(quadVertices));
+
+ setupShaders();
+ uploadTexture();
+}
+
+void DDSWidget::setupShaders()
+{
+ m_shader = new QOpenGLShaderProgram(this);
+ m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSrc);
+
+ if (m_dds.isCubemap()) {
+ m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, cubemapFragSrc);
+ } else {
+ m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSrc);
+ }
+
+ m_shader->link();
+}
+
+void DDSWidget::uploadTexture()
+{
+ if (m_dds.faceCount() == 0 || m_dds.mipCount() == 0)
+ return;
+
+ const auto& fmt = m_dds.glFormat();
+
+ if (m_dds.isCubemap()) {
+ m_texture = new QOpenGLTexture(QOpenGLTexture::TargetCubeMap);
+ } else {
+ m_texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
+ }
+
+ m_texture->setAutoMipMapGenerationEnabled(false);
+ m_texture->setMipLevels(m_dds.mipCount());
+
+ if (m_dds.isCubemap()) {
+ m_texture->setSize(m_dds.width(), m_dds.height());
+ // Set format for allocation
+ if (fmt.compressed) {
+ m_texture->setFormat(
+ static_cast<QOpenGLTexture::TextureFormat>(fmt.internalFormat));
+ } else {
+ m_texture->setFormat(
+ static_cast<QOpenGLTexture::TextureFormat>(fmt.internalFormat));
+ }
+ m_texture->allocateStorage();
+
+ static const QOpenGLTexture::CubeMapFace cubeMapFaces[] = {
+ QOpenGLTexture::CubeMapPositiveX, QOpenGLTexture::CubeMapNegativeX,
+ QOpenGLTexture::CubeMapPositiveY, QOpenGLTexture::CubeMapNegativeY,
+ QOpenGLTexture::CubeMapPositiveZ, QOpenGLTexture::CubeMapNegativeZ,
+ };
+
+ int numFaces = std::min(m_dds.faceCount(), 6);
+ for (int f = 0; f < numFaces; ++f) {
+ const auto& face = m_dds.face(f);
+ for (int m = 0; m < face.mips.size(); ++m) {
+ const auto& mip = face.mips[m];
+ QByteArray pixelData = mip.data;
+ if (fmt.converter) {
+ pixelData = fmt.converter(mip.data, mip.width, mip.height);
+ }
+ if (fmt.compressed) {
+ m_texture->setCompressedData(
+ m, 0, cubeMapFaces[f],
+ pixelData.size(),
+ pixelData.constData());
+ } else {
+ m_texture->setData(
+ m, 0, cubeMapFaces[f],
+ static_cast<QOpenGLTexture::PixelFormat>(fmt.format),
+ static_cast<QOpenGLTexture::PixelType>(fmt.type),
+ pixelData.constData());
+ }
+ }
+ }
+ } else {
+ m_texture->setSize(m_dds.width(), m_dds.height());
+ if (fmt.compressed) {
+ m_texture->setFormat(
+ static_cast<QOpenGLTexture::TextureFormat>(fmt.internalFormat));
+ } else {
+ m_texture->setFormat(
+ static_cast<QOpenGLTexture::TextureFormat>(fmt.internalFormat));
+ }
+ m_texture->allocateStorage();
+
+ const auto& face = m_dds.face(0);
+ for (int m = 0; m < face.mips.size(); ++m) {
+ const auto& mip = face.mips[m];
+ QByteArray pixelData = mip.data;
+ if (fmt.converter) {
+ pixelData = fmt.converter(mip.data, mip.width, mip.height);
+ }
+ if (fmt.compressed) {
+ m_texture->setCompressedData(
+ m, 0,
+ pixelData.size(),
+ pixelData.constData());
+ } else {
+ m_texture->setData(
+ m, 0,
+ static_cast<QOpenGLTexture::PixelFormat>(fmt.format),
+ static_cast<QOpenGLTexture::PixelType>(fmt.type),
+ pixelData.constData());
+ }
+ }
+ }
+
+ m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
+ if (fmt.sampler != SamplerType::Float) {
+ m_texture->setMinMagFilters(QOpenGLTexture::Nearest,
+ QOpenGLTexture::Nearest);
+ } else {
+ m_texture->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,
+ QOpenGLTexture::Linear);
+ }
+}
+
+void DDSWidget::resizeGL(int w, int h)
+{
+ glViewport(0, 0, w, h);
+}
+
+void DDSWidget::paintGL()
+{
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ if (!m_texture || !m_shader)
+ return;
+
+ m_shader->bind();
+ m_texture->bind();
+
+ float widgetAspect = width() > 0 && height() > 0
+ ? static_cast<float>(width()) / height()
+ : 1.0f;
+ m_shader->setUniformValue("uAspect", m_aspectRatio);
+ m_shader->setUniformValue("uWidgetAspect", widgetAspect);
+ m_shader->setUniformValue("uTexture", 0);
+
+ m_vbo.bind();
+ m_shader->enableAttributeArray(0);
+ m_shader->enableAttributeArray(1);
+ m_shader->setAttributeBuffer(0, GL_FLOAT, 0, 2, 4 * sizeof(float));
+ m_shader->setAttributeBuffer(1, GL_FLOAT, 2 * sizeof(float), 2,
+ 4 * sizeof(float));
+
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+ m_shader->disableAttributeArray(0);
+ m_shader->disableAttributeArray(1);
+ m_vbo.release();
+ m_texture->release();
+ m_shader->release();
+}
diff --git a/libs/preview_dds_native/src/ddswidget.h b/libs/preview_dds_native/src/ddswidget.h
new file mode 100644
index 0000000..968e253
--- /dev/null
+++ b/libs/preview_dds_native/src/ddswidget.h
@@ -0,0 +1,36 @@
+#ifndef DDSWIDGET_H
+#define DDSWIDGET_H
+
+#include "ddsfile.h"
+
+#include <QOpenGLBuffer>
+#include <QOpenGLFunctions>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLTexture>
+#include <QOpenGLWidget>
+
+class DDSWidget : public QOpenGLWidget, protected QOpenGLFunctions
+{
+ Q_OBJECT
+
+public:
+ explicit DDSWidget(const DDSFile& dds, QWidget* parent = nullptr);
+ ~DDSWidget() override;
+
+protected:
+ void initializeGL() override;
+ void resizeGL(int w, int h) override;
+ void paintGL() override;
+
+private:
+ void uploadTexture();
+ void setupShaders();
+
+ const DDSFile& m_dds;
+ QOpenGLTexture* m_texture = nullptr;
+ QOpenGLShaderProgram* m_shader = nullptr;
+ QOpenGLBuffer m_vbo;
+ float m_aspectRatio = 1.0f;
+};
+
+#endif // DDSWIDGET_H