#include "imagepreviewwidget.h" #include #include #include #include #include #include #include #include "statusbarmanager.h" ImagePreviewWidget::ImagePreviewWidget(QWidget *parent) : QWidget(parent) , mImageLabel(new QLabel(this)) , mInfoLabel(new QLabel(this)) , mScrollArea(new QScrollArea(this)) , mDragging(false) , mZoomFactor(1.0) { mImageLabel->setAlignment(Qt::AlignCenter); mImageLabel->setBackgroundRole(QPalette::Base); mImageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); mImageLabel->setScaledContents(false); mScrollArea->setWidget(mImageLabel); mScrollArea->setWidgetResizable(false); // Don't auto-resize, allow scrolling mScrollArea->setBackgroundRole(QPalette::Dark); mScrollArea->setAlignment(Qt::AlignCenter); mScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mInfoLabel->setAlignment(Qt::AlignCenter); mInfoLabel->setStyleSheet("QLabel { background-color: #333; color: #fff; padding: 4px; }"); QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(mInfoLabel); layout->addWidget(mScrollArea, 1); setLayout(layout); // Enable mouse tracking for drag-to-pan setMouseTracking(true); mScrollArea->setMouseTracking(true); mScrollArea->viewport()->installEventFilter(this); mScrollArea->viewport()->setCursor(Qt::OpenHandCursor); } void ImagePreviewWidget::setFilename(const QString &filename) { mFilename = filename; } QSize ImagePreviewWidget::imageSize() const { return mImageSize; } bool ImagePreviewWidget::exportDebugImage(const QString &suffix) { QPixmap pixmap = mImageLabel->pixmap(); if (pixmap.isNull()) { return false; } QDir().mkdir("exports"); QString filename = mFilename; if (filename.isEmpty()) { filename = "debug_image"; } // Remove extension and add suffix int dotPos = filename.lastIndexOf('.'); if (dotPos > 0) { filename = filename.left(dotPos); } if (!suffix.isEmpty()) { filename += "_" + suffix; } filename += "_DEBUG.png"; QString path = "exports/" + filename; bool success = pixmap.save(path, "PNG"); if (success) { StatusBarManager::instance().updateStatus(QString("Exported debug image: %1").arg(path), 2000); } return success; } bool ImagePreviewWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == mScrollArea->viewport()) { if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *me = static_cast(event); if (me->button() == Qt::LeftButton) { mDragging = true; mLastDragPos = me->globalPosition().toPoint(); mScrollArea->viewport()->setCursor(Qt::ClosedHandCursor); return true; } } else if (event->type() == QEvent::MouseMove) { if (mDragging) { QMouseEvent *me = static_cast(event); QPoint globalPos = me->globalPosition().toPoint(); QPoint delta = globalPos - mLastDragPos; mLastDragPos = globalPos; QScrollBar *hBar = mScrollArea->horizontalScrollBar(); QScrollBar *vBar = mScrollArea->verticalScrollBar(); hBar->setValue(hBar->value() - delta.x()); vBar->setValue(vBar->value() - delta.y()); return true; } } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent *me = static_cast(event); if (me->button() == Qt::LeftButton) { mDragging = false; mScrollArea->viewport()->setCursor(Qt::OpenHandCursor); return true; } } else if (event->type() == QEvent::Wheel) { QWheelEvent *we = static_cast(event); int delta = we->angleDelta().y(); if (delta > 0) { mZoomFactor *= 1.15; } else if (delta < 0) { mZoomFactor /= 1.15; } mZoomFactor = qBound(0.1, mZoomFactor, 10.0); updateZoom(); return true; } } return QWidget::eventFilter(obj, event); } void ImagePreviewWidget::wheelEvent(QWheelEvent *event) { // Forward to viewport event filter int delta = event->angleDelta().y(); if (delta > 0) { mZoomFactor *= 1.15; } else if (delta < 0) { mZoomFactor /= 1.15; } mZoomFactor = qBound(0.1, mZoomFactor, 10.0); updateZoom(); event->accept(); } void ImagePreviewWidget::updateZoom() { if (mOriginalPixmap.isNull()) return; QSize newSize = mOriginalPixmap.size() * mZoomFactor; QPixmap scaled = mOriginalPixmap.scaled(newSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); mImageLabel->setPixmap(scaled); mImageLabel->adjustSize(); // Update info label with zoom level mInfoLabel->setText(QString("%1 - %2x%3 (%.0f%%)") .arg(mFilename) .arg(mImageSize.width()) .arg(mImageSize.height()) .arg(mZoomFactor * 100)); } bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &format) { // Check for Xbox texture formats (XBTX2D, TX2D, etc.) if (data.size() >= 8) { QString magic = QString::fromLatin1(data.left(8)); if (magic.startsWith("XBTX2D") || magic.startsWith("TX2D") || magic.startsWith("TX1D") || magic.startsWith("TX3D")) { // Try to decode Xbox 360 texture QImage image = loadXBTX2D(data); if (!image.isNull()) { mImageSize = image.size(); mOriginalPixmap = QPixmap::fromImage(image); mZoomFactor = 1.0; mImageLabel->setPixmap(mOriginalPixmap); mImageLabel->adjustSize(); mInfoLabel->setText(QString("%1 - %2x%3 (Xbox 360)").arg(mFilename).arg(image.width()).arg(image.height())); return true; } // Fallback to info display if decode fails mImageLabel->setText(QString("Xbox 360 Texture Format\n\nMagic: %1\nSize: %2 bytes\n\nCould not decode texture.") .arg(magic.left(6).trimmed()) .arg(data.size())); mInfoLabel->setText(QString("%1 - Xbox 360 Texture (%2 bytes)").arg(mFilename).arg(data.size())); return true; } } // Check for DDS format (starts with "DDS ") if (data.size() >= 4 && data.left(4) == "DDS ") { mImageLabel->setText(QString("DDS Texture Format\n\nSize: %1 bytes\n\nDDS preview not yet implemented.") .arg(data.size())); mInfoLabel->setText(QString("%1 - DDS Texture (%2 bytes)").arg(mFilename).arg(data.size())); return true; } QImage image; // Try standard Qt loading first if (!format.isEmpty()) { image.loadFromData(data, format.toUtf8().constData()); } if (image.isNull()) { image.loadFromData(data); } // If still null and looks like TGA, try manual loading if (image.isNull() && (format.toLower() == "tga" || (data.size() > 18 && mFilename.toLower().endsWith(".tga")))) { image = loadTGA(data); } if (image.isNull()) { // Show hex dump of first bytes for debugging QString hexDump; for (int i = 0; i < qMin(16, data.size()); i++) { hexDump += QString("%1 ").arg((unsigned char)data[i], 2, 16, QChar('0')); } mImageLabel->setText(QString("Failed to load image\n\nFirst bytes: %1\nSize: %2 bytes") .arg(hexDump.trimmed()) .arg(data.size())); mInfoLabel->setText(QString("%1 - Load failed").arg(mFilename)); return false; } mImageSize = image.size(); mOriginalPixmap = QPixmap::fromImage(image); mZoomFactor = 1.0; mImageLabel->setPixmap(mOriginalPixmap); mImageLabel->adjustSize(); QString info = QString("%1 - %2x%3 - %4 bytes") .arg(mFilename) .arg(image.width()) .arg(image.height()) .arg(data.size()); mInfoLabel->setText(info); return true; } bool ImagePreviewWidget::loadFromFile(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return false; } mFilename = QFileInfo(path).fileName(); QByteArray data = file.readAll(); QString format; if (path.toLower().endsWith(".tga")) format = "TGA"; else if (path.toLower().endsWith(".dds")) format = "DDS"; else if (path.toLower().endsWith(".png")) format = "PNG"; else if (path.toLower().endsWith(".jpg") || path.toLower().endsWith(".jpeg")) format = "JPG"; else if (path.toLower().endsWith(".bmp")) format = "BMP"; return loadFromData(data, format); } QImage ImagePreviewWidget::loadTGA(const QByteArray &data) { // Simple TGA loader for uncompressed RGB/RGBA images if (data.size() < 18) return QImage(); const uchar *d = reinterpret_cast(data.constData()); // TGA header uchar idLength = d[0]; uchar colorMapType = d[1]; uchar imageType = d[2]; // Skip color map spec (5 bytes: 3-7) // Image spec starts at byte 8 quint16 xOrigin = d[8] | (d[9] << 8); quint16 yOrigin = d[10] | (d[11] << 8); quint16 width = d[12] | (d[13] << 8); quint16 height = d[14] | (d[15] << 8); uchar bitsPerPixel = d[16]; uchar descriptor = d[17]; Q_UNUSED(xOrigin); Q_UNUSED(yOrigin); Q_UNUSED(colorMapType); // Only support uncompressed true-color (type 2) or grayscale (type 3) if (imageType != 2 && imageType != 3 && imageType != 10) { return QImage(); } int bytesPerPixel = bitsPerPixel / 8; if (bytesPerPixel < 1 || bytesPerPixel > 4) { return QImage(); } int dataOffset = 18 + idLength; int expectedSize = dataOffset + width * height * bytesPerPixel; // For RLE images, we can't easily predict size, so just check minimum if (imageType != 10 && data.size() < expectedSize) { return QImage(); } QImage::Format format = (bytesPerPixel == 4) ? QImage::Format_ARGB32 : QImage::Format_RGB32; QImage image(width, height, format); if (image.isNull()) return QImage(); const uchar *src = d + dataOffset; bool flipVertical = !(descriptor & 0x20); // Bit 5: origin is top-left if set if (imageType == 2 || imageType == 3) { // Uncompressed for (int y = 0; y < height; y++) { int destY = flipVertical ? (height - 1 - y) : y; QRgb *destLine = reinterpret_cast(image.scanLine(destY)); for (int x = 0; x < width; x++) { int idx = (y * width + x) * bytesPerPixel; if (dataOffset + idx + bytesPerPixel > data.size()) break; uchar b = src[idx]; uchar g = (bytesPerPixel > 1) ? src[idx + 1] : b; uchar r = (bytesPerPixel > 2) ? src[idx + 2] : b; uchar a = (bytesPerPixel > 3) ? src[idx + 3] : 255; destLine[x] = qRgba(r, g, b, a); } } } else if (imageType == 10) { // RLE compressed int pixelCount = width * height; int currentPixel = 0; int srcIdx = 0; int maxSrcIdx = data.size() - dataOffset; while (currentPixel < pixelCount && srcIdx < maxSrcIdx) { uchar header = src[srcIdx++]; int count = (header & 0x7F) + 1; if (header & 0x80) { // RLE packet if (srcIdx + bytesPerPixel > maxSrcIdx) break; uchar b = src[srcIdx]; uchar g = (bytesPerPixel > 1) ? src[srcIdx + 1] : b; uchar r = (bytesPerPixel > 2) ? src[srcIdx + 2] : b; uchar a = (bytesPerPixel > 3) ? src[srcIdx + 3] : 255; srcIdx += bytesPerPixel; QRgb color = qRgba(r, g, b, a); for (int i = 0; i < count && currentPixel < pixelCount; i++, currentPixel++) { int x = currentPixel % width; int y = currentPixel / width; int destY = flipVertical ? (height - 1 - y) : y; QRgb *destLine = reinterpret_cast(image.scanLine(destY)); destLine[x] = color; } } else { // Raw packet for (int i = 0; i < count && currentPixel < pixelCount; i++, currentPixel++) { if (srcIdx + bytesPerPixel > maxSrcIdx) break; uchar b = src[srcIdx]; uchar g = (bytesPerPixel > 1) ? src[srcIdx + 1] : b; uchar r = (bytesPerPixel > 2) ? src[srcIdx + 2] : b; uchar a = (bytesPerPixel > 3) ? src[srcIdx + 3] : 255; srcIdx += bytesPerPixel; int x = currentPixel % width; int y = currentPixel / width; int destY = flipVertical ? (height - 1 - y) : y; QRgb *destLine = reinterpret_cast(image.scanLine(destY)); destLine[x] = qRgba(r, g, b, a); } } } } return image; } // DXT1 block decoder - read colors as big-endian (Xbox 360 native) static void decodeDXT1BlockStatic(const uchar *src, QRgb *dest, int destPitch) { // Read colors as big-endian (Xbox 360 native format) quint16 c0 = (src[0] << 8) | src[1]; quint16 c1 = (src[2] << 8) | src[3]; // Extract RGB565 components: bits 15-11=R, 10-5=G, 4-0=B // Xbox 360 stores as BGR565, so swap R and B int b0 = ((c0 >> 11) & 0x1F) * 255 / 31; int g0 = ((c0 >> 5) & 0x3F) * 255 / 63; int r0 = (c0 & 0x1F) * 255 / 31; int b1 = ((c1 >> 11) & 0x1F) * 255 / 31; int g1 = ((c1 >> 5) & 0x3F) * 255 / 63; int r1 = (c1 & 0x1F) * 255 / 31; // Build color table QRgb colors[4]; colors[0] = qRgb(r0, g0, b0); colors[1] = qRgb(r1, g1, b1); if (c0 > c1) { colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3); colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3); } else { colors[2] = qRgb((r0 + r1) / 2, (g0 + g1) / 2, (b0 + b1) / 2); colors[3] = qRgba(0, 0, 0, 0); // Transparent } // Read indices - standard order (no swap needed) for (int y = 0; y < 4; y++) { uchar rowByte = src[4 + y]; for (int x = 0; x < 4; x++) { int idx = (rowByte >> (x * 2)) & 0x03; dest[y * destPitch + x] = colors[idx]; } } } // DXT5 block decoder - read as big-endian (Xbox 360 native) static void decodeDXT5BlockStatic(const uchar *src, QRgb *dest, int destPitch) { // Alpha block (first 8 bytes) - standard order uchar alpha0 = src[0]; uchar alpha1 = src[1]; // Build alpha table uchar alphas[8]; alphas[0] = alpha0; alphas[1] = alpha1; if (alpha0 > alpha1) { alphas[2] = (6 * alpha0 + 1 * alpha1) / 7; alphas[3] = (5 * alpha0 + 2 * alpha1) / 7; alphas[4] = (4 * alpha0 + 3 * alpha1) / 7; alphas[5] = (3 * alpha0 + 4 * alpha1) / 7; alphas[6] = (2 * alpha0 + 5 * alpha1) / 7; alphas[7] = (1 * alpha0 + 6 * alpha1) / 7; } else { alphas[2] = (4 * alpha0 + 1 * alpha1) / 5; alphas[3] = (3 * alpha0 + 2 * alpha1) / 5; alphas[4] = (2 * alpha0 + 3 * alpha1) / 5; alphas[5] = (1 * alpha0 + 4 * alpha1) / 5; alphas[6] = 0; alphas[7] = 255; } // Alpha indices (6 bytes, 48 bits = 16 pixels * 3 bits each) - standard order quint64 alphaIndices = src[2] | ((quint64)src[3] << 8) | ((quint64)src[4] << 16) | ((quint64)src[5] << 24) | ((quint64)src[6] << 32) | ((quint64)src[7] << 40); // Color block (bytes 8-15, same as DXT1) - big-endian const uchar *colorSrc = src + 8; quint16 c0 = (colorSrc[0] << 8) | colorSrc[1]; // Big-endian (Xbox 360) quint16 c1 = (colorSrc[2] << 8) | colorSrc[3]; // Big-endian (Xbox 360) // BGR565: bits 15-11=B, 10-5=G, 4-0=R (Xbox 360 swapped) int b0 = ((c0 >> 11) & 0x1F) * 255 / 31; int g0 = ((c0 >> 5) & 0x3F) * 255 / 63; int r0 = (c0 & 0x1F) * 255 / 31; int b1 = ((c1 >> 11) & 0x1F) * 255 / 31; int g1 = ((c1 >> 5) & 0x3F) * 255 / 63; int r1 = (c1 & 0x1F) * 255 / 31; QRgb colors[4]; colors[0] = qRgb(r0, g0, b0); colors[1] = qRgb(r1, g1, b1); colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3); colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3); // Decode 4x4 block - standard order for (int y = 0; y < 4; y++) { uchar colorRowByte = colorSrc[4 + y]; for (int x = 0; x < 4; x++) { int pixelIdx = y * 4 + x; int alphaIdx = (alphaIndices >> (pixelIdx * 3)) & 0x07; int colorIdx = (colorRowByte >> (x * 2)) & 0x03; QRgb color = colors[colorIdx]; dest[y * destPitch + x] = qRgba(qRed(color), qGreen(color), qBlue(color), alphas[alphaIdx]); } } } // Swap endianness in 16-bit chunks (required for Xbox 360) static void swapEndian16(uchar *data, int size) { for (int i = 0; i < size - 1; i += 2) { uchar tmp = data[i]; data[i] = data[i + 1]; data[i + 1] = tmp; } } // Xbox 360 XGAddress2DTiledX - convert tiled offset to linear X // From Noesis inc_xbox360_untile.py (credits: Banz99's Nier-Texture-Manager, GTA XTD editor) static unsigned int xgAddress2DTiledX(unsigned int offset, unsigned int width, unsigned int texelPitch) { unsigned int alignedWidth = (width + 31) & ~31; unsigned int logBpp = (texelPitch >> 2) + ((texelPitch >> 1) >> (texelPitch >> 2)); unsigned int offsetB = offset << logBpp; unsigned int offsetT = ((offsetB & ~4095) >> 3) + ((offsetB & 1792) >> 2) + (offsetB & 63); unsigned int offsetM = offsetT >> (7 + logBpp); unsigned int macroX = ((offsetM % (alignedWidth >> 5)) << 2); unsigned int tile = ((((offsetT >> (5 + logBpp)) & 2) + (offsetB >> 6)) & 3); unsigned int macro = (macroX + tile) << 3; unsigned int micro = ((((offsetT >> 1) & ~15) + (offsetT & 15)) & ((texelPitch << 3) - 1)) >> logBpp; return macro + micro; } // Xbox 360 XGAddress2DTiledY - convert tiled offset to linear Y static unsigned int xgAddress2DTiledY(unsigned int offset, unsigned int width, unsigned int texelPitch) { unsigned int alignedWidth = (width + 31) & ~31; unsigned int logBpp = (texelPitch >> 2) + ((texelPitch >> 1) >> (texelPitch >> 2)); unsigned int offsetB = offset << logBpp; unsigned int offsetT = ((offsetB & ~4095) >> 3) + ((offsetB & 1792) >> 2) + (offsetB & 63); unsigned int offsetM = offsetT >> (7 + logBpp); unsigned int macroY = ((offsetM / (alignedWidth >> 5)) << 2); unsigned int tile = ((offsetT >> (6 + logBpp)) & 1) + (((offsetB & 2048) >> 10)); unsigned int macro = (macroY + tile) << 3; unsigned int micro = ((((offsetT & (((texelPitch << 6) - 1) & ~31)) + ((offsetT & 15) << 1)) >> (3 + logBpp)) & ~1); return macro + micro + ((offsetT & 16) >> 4); } // Xbox 360 texture untiling - iterate tiled source, write to linear dest // Based on official Xbox 360 SDK XGAddress2DTiledX/Y functions static void untileXbox360(const uchar *src, uchar *dest, int width, int height, int blockSize) { // For DXT, work in blocks (4x4 pixels per block) unsigned int blocksWide = (width + 3) / 4; unsigned int blocksHigh = (height + 3) / 4; // Calculate aligned dimensions for iteration (SDK aligns internally, but we need to iterate aligned space) unsigned int alignedBlocksWide = (blocksWide + 31) & ~31; unsigned int alignedBlocksHigh = (blocksHigh + 31) & ~31; unsigned int totalAlignedBlocks = alignedBlocksWide * alignedBlocksHigh; // texelPitch is the block size (8 for DXT1, 16 for DXT5) unsigned int texelPitch = blockSize; // Iterate through aligned source blocks for (unsigned int blockOffset = 0; blockOffset < totalAlignedBlocks; blockOffset++) { // Pass the ACTUAL width in blocks - XGAddress functions align internally! // This is the key fix: SDK expects real width, not pre-aligned unsigned int x = xgAddress2DTiledX(blockOffset, blocksWide, texelPitch); unsigned int y = xgAddress2DTiledY(blockOffset, blocksWide, texelPitch); // Skip blocks outside actual texture bounds if (x >= blocksWide || y >= blocksHigh) { continue; } // Source is sequential tiled (aligned), dest is linear (actual size) unsigned int srcOffset = blockOffset * texelPitch; unsigned int destOffset = (y * blocksWide + x) * texelPitch; memcpy(dest + destOffset, src + srcOffset, texelPitch); } } // Helper to decode DXT data into an image static QImage decodeDXTToImage(const uchar *srcData, int width, int height, int blockSize, bool isDXT5, int textureDataSize) { int blocksWide = (width + 3) / 4; int blocksHigh = (height + 3) / 4; QImage image(width, height, QImage::Format_ARGB32); if (image.isNull()) return QImage(); image.fill(Qt::black); for (int by = 0; by < blocksHigh; by++) { for (int bx = 0; bx < blocksWide; bx++) { int blockIdx = by * blocksWide + bx; int blockOffset = blockIdx * blockSize; if (blockOffset + blockSize > textureDataSize) break; const uchar *blockData = srcData + blockOffset; // Decode into temporary 4x4 block QRgb block[16]; if (isDXT5) { decodeDXT5BlockStatic(blockData, block, 4); } else { decodeDXT1BlockStatic(blockData, block, 4); } // Copy block to image int px = bx * 4; int py = by * 4; for (int y = 0; y < 4 && py + y < height; y++) { QRgb *destLine = reinterpret_cast(image.scanLine(py + y)); for (int x = 0; x < 4 && px + x < width; x++) { destLine[px + x] = block[y * 4 + x]; } } } } return image; } QImage ImagePreviewWidget::loadXBTX2D(const QByteArray &data) { if (data.size() < 0x60) return QImage(); const uchar *d = reinterpret_cast(data.constData()); // XBTX2D header (big-endian) - actual format from hex analysis: // 0x00-0x07: Magic "XBTX2D " (8 bytes) // 0x08-0x0B: Version (4 bytes, BE) // 0x0C-0x0F: Width (4 bytes, BE) // 0x10-0x13: Height (4 bytes, BE) // 0x14-0x17: Hash/ID // 0x18-0x1B: Mip count (4 bytes, BE) // 0x1C-0x1F: Size field 1 // 0x20-0x23: Size field 2 (base mip size for DXT1) // 0x24-0x27: Format (3=DXT1?) // 0x58: Data start quint32 width = (d[0x0C] << 24) | (d[0x0D] << 16) | (d[0x0E] << 8) | d[0x0F]; quint32 height = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13]; quint32 baseMipSize = (d[0x20] << 24) | (d[0x21] << 16) | (d[0x22] << 8) | d[0x23]; // Sanity checks if (width == 0 || height == 0 || width > 8192 || height > 8192) { return QImage(); } // Determine format from base mip size // DXT1: 8 bytes per 4x4 block = width*height/2 // DXT5: 16 bytes per 4x4 block = width*height int blockSize; int dataOffset = 0x58; // Fixed header size bool isDXT5; quint32 expectedDXT1Size = (width * height) / 2; quint32 expectedDXT5Size = width * height; if (baseMipSize == expectedDXT5Size) { // DXT5 (or similar 16-byte block format) blockSize = 16; isDXT5 = true; StatusBarManager::instance().updateStatus( QString("XBTX2D: %1 - DXT5 %2x%3").arg(mFilename).arg(width).arg(height), 3000); } else { // DXT1 (or similar 8-byte block format) blockSize = 8; isDXT5 = false; StatusBarManager::instance().updateStatus( QString("XBTX2D: %1 - DXT1 %2x%3").arg(mFilename).arg(width).arg(height), 3000); } int blocksWide = (width + 3) / 4; int blocksHigh = (height + 3) / 4; int totalBlocks = blocksWide * blocksHigh; int textureDataSize = totalBlocks * blockSize; if (data.size() < dataOffset) return QImage(); int availableData = data.size() - dataOffset; if (availableData < textureDataSize) { textureDataSize = availableData; } const uchar *srcData = d + dataOffset; // Xbox 360 DXT textures require: // 1. Untiling (GPU memory layout to linear) // 2. DXT decode with big-endian color reading // Try both untiled and linear to compare QByteArray untiled(textureDataSize, 0); untileXbox360(srcData, reinterpret_cast(untiled.data()), width, height, blockSize); // Decode both versions QImage untiledResult = decodeDXTToImage(reinterpret_cast(untiled.constData()), width, height, blockSize, isDXT5, textureDataSize); QImage linearResult = decodeDXTToImage(srcData, width, height, blockSize, isDXT5, textureDataSize); // Export both for comparison QDir().mkdir("exports"); QString debugName = mFilename.isEmpty() ? "xbtx2d_debug" : mFilename; int dot = debugName.lastIndexOf('.'); if (dot > 0) debugName = debugName.left(dot); if (!untiledResult.isNull()) { untiledResult.save("exports/" + debugName + "_UNTILED.png", "PNG"); } if (!linearResult.isNull()) { linearResult.save("exports/" + debugName + "_LINEAR.png", "PNG"); } // Return the untiled version return untiledResult; }