Revision control
++ // Copyright (C) 2008-2012 NVIDIA Corporation.
++ // Copyright (C) 2019 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderloadedtexture_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendererutil_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendertexturedata_p.h>
++ #include <QtGui/QImageReader>
++ #include <QtGui/QColorSpace>
++ #include <QtCore/qnumeric.h>
++ #include <QtMath>
++
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include <private/qtexturefilereader_p.h>
++
++ #define TINYEXR_IMPLEMENTATION
++ #define TINYEXR_USE_MINIZ 0
++ #define TINYEXR_USE_THREAD 1
++ #include <zlib.h>
++ QT_WARNING_PUSH
++ QT_WARNING_DISABLE_CLANG("-Wunused-parameter")
++ QT_WARNING_DISABLE_CLANG("-Wunused-function")
++ QT_WARNING_DISABLE_GCC("-Wunused-parameter")
++ QT_WARNING_DISABLE_GCC("-Wunused-function")
++ #include <tinyexr.h>
++ QT_WARNING_POP
++ #include "qssglightmapio_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++
++ QSharedPointer<QIODevice> QSSGInputUtil::getStreamForFile(const QString &inPath, bool inQuiet, QString *outPath)
++ {
++ QFile *file = nullptr;
++ QString tryPath = inPath;
++ if (tryPath.startsWith(QLatin1String("qrc:/"))) {
++ tryPath = inPath.mid(3);
++ } else if (tryPath.startsWith(QLatin1String("file://"))) {
++ tryPath = inPath.mid(7);
++ }
++
++ QFileInfo fi(tryPath);
++ bool found = fi.exists();
++ if (!found && fi.isNativePath()) {
++ tryPath.prepend(QLatin1String(":/"));
++ fi.setFile(tryPath);
++ found = fi.exists();
++ }
++ if (found) {
++ QString filePath = fi.canonicalFilePath();
++ file = new QFile(filePath);
++ if (file->open(QIODevice::ReadOnly)) {
++ if (outPath)
++ *outPath = filePath;
++ } else {
++ delete file;
++ file = nullptr;
++ }
++ }
++ if (!file && !inQuiet)
++ qCWarning(WARNING, "Failed to find file: %s", qPrintable(inPath));
++ return QSharedPointer<QIODevice>(file);
++ }
++
++ QSharedPointer<QIODevice> QSSGInputUtil::getStreamForTextureFile(const QString &inPath, bool inQuiet,
++ QString *outPath, FileType *outFileType)
++ {
++ static const QList<QByteArray> hdrFormats = QList<QByteArray>({ "hdr", "exr" });
++ static const QList<QByteArray> textureFormats = QTextureFileReader::supportedFileFormats();
++ static const QList<QByteArray> imageFormats = QImageReader::supportedImageFormats();
++ static const QList<QByteArray> allFormats = textureFormats + hdrFormats + imageFormats;
++
++ QString filePath;
++ QByteArray ext;
++ QSharedPointer<QIODevice> stream = getStreamForFile(inPath, true, &filePath);
++ if (stream) {
++ ext = QFileInfo(filePath).suffix().toLatin1().toLower();
++ } else {
++ for (const QByteArray &format : allFormats) {
++ QString tryName = inPath + QLatin1Char('.') + QLatin1String(format);
++ stream = getStreamForFile(tryName, true, &filePath);
++ if (stream) {
++ ext = format;
++ break;
++ }
++ }
++ }
++ if (stream) {
++ if (outPath)
++ *outPath = filePath;
++ if (outFileType) {
++ FileType type = UnknownFile;
++ if (hdrFormats.contains(ext))
++ type = HdrFile;
++ else if (textureFormats.contains(ext))
++ type = TextureFile;
++ else if (imageFormats.contains(ext))
++ type = ImageFile;
++ *outFileType = type;
++ }
++ } else if (!inQuiet) {
++ qCWarning(WARNING, "Failed to find texture file for: %s", qPrintable(inPath));
++ }
++ return stream;
++ }
++
++ static inline QSSGRenderTextureFormat fromGLtoTextureFormat(quint32 internalFormat)
++ {
++ switch (internalFormat) {
++ case 0x8229:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R8);
++ case 0x822A:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R16);
++ case 0x822D:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R16F);
++ case 0x8235:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32I);
++ case 0x8236:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32UI);
++ case 0x822E:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R32F);
++ case 0x822B:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG8);
++ case 0x8058:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8);
++ case 0x8051:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8);
++ case 0x8C41:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8);
++ case 0x8C43:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8A8);
++ case 0x8D62:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB565);
++ case 0x803C:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Alpha8);
++ case 0x8040:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Luminance8);
++ case 0x8042:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Luminance16);
++ case 0x8045:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::LuminanceAlpha8);
++ case 0x881A:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16F);
++ case 0x822F:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG16F);
++ case 0x8230:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG32F);
++ case 0x8815:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32F);
++ case 0x8814:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32F);
++ case 0x8C3A:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11G11B10);
++ case 0x8C3D:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB9E5);
++ case 0x8059:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB10_A2);
++ case 0x881B:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16F);
++ case 0x8D70:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32UI);
++ case 0x8D71:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32UI);
++ case 0x8D76:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16UI);
++ case 0x8D77:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16UI);
++ case 0x8D7C:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8UI);
++ case 0x8D7D:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8UI);
++ case 0x8D82:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA32I);
++ case 0x8D83:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB32I);
++ case 0x8D88:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA16I);
++ case 0x8D89:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB16I);
++ case 0x8D8E:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8I);
++ case 0x8D8F:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8I);
++ case 0x83F1:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT1);
++ case 0x83F0:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB_DXT1);
++ case 0x83F2:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT3);
++ case 0x83F3:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_DXT5);
++ case 0x9270:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11_EAC_UNorm);
++ case 0x9271:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::R11_EAC_SNorm);
++ case 0x9272:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG11_EAC_UNorm);
++ case 0x9273:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RG11_EAC_SNorm);
++ case 0x9274:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8_ETC2);
++ case 0x9275:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_ETC2);
++ case 0x9276:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGB8_PunchThrough_Alpha1_ETC2);
++ case 0x9277:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_PunchThrough_Alpha1_ETC2);
++ case 0x9278:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA8_ETC2_EAC);
++ case 0x9279:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ETC2_EAC);
++ case 0x93B0:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_4x4);
++ case 0x93B1:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_5x4);
++ case 0x93B2:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_5x5);
++ case 0x93B3:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_6x5);
++ case 0x93B4:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_6x6);
++ case 0x93B5:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x5);
++ case 0x93B6:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x6);
++ case 0x93B7:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_8x8);
++ case 0x93B8:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x5);
++ case 0x93B9:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x6);
++ case 0x93BA:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x8);
++ case 0x93BB:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_10x10);
++ case 0x93BC:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_12x10);
++ case 0x93BD:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::RGBA_ASTC_12x12);
++ case 0x93D0:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_4x4);
++ case 0x93D1:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x4);
++ case 0x93D2:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x5);
++ case 0x93D3:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x5);
++ case 0x93D4:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x6);
++ case 0x93D5:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x5);
++ case 0x93D6:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x6);
++ case 0x93D7:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x8);
++ case 0x93D8:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x5);
++ case 0x93D9:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x6);
++ case 0x93DA:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x8);
++ case 0x93DB:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x10);
++ case 0x93DC:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x10);
++ case 0x93DD:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x12);
++ case 0x81A5:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth16);
++ case 0x81A6:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth24);
++ case 0x81A7:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth32);
++ case 0x88F0:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Depth24Stencil8);
++ default:
++ return QSSGRenderTextureFormat(QSSGRenderTextureFormat::Unknown);
++ }
++ }
++
++ static QImage loadImage(const QString &inPath, bool flipVertical)
++ {
++ QImage image(inPath);
++ if (image.isNull())
++ return image;
++ const QPixelFormat pixFormat = image.pixelFormat();
++ QImage::Format targetFormat = QImage::Format_RGBA8888_Premultiplied;
++ if (image.colorCount()) // a palleted image
++ targetFormat = QImage::Format_RGBA8888;
++ else if (pixFormat.channelCount() == 1)
++ targetFormat = QImage::Format_Grayscale8;
++ else if (pixFormat.alphaUsage() == QPixelFormat::IgnoresAlpha)
++ targetFormat = QImage::Format_RGBX8888;
++ else if (pixFormat.premultiplied() == QPixelFormat::NotPremultiplied)
++ targetFormat = QImage::Format_RGBA8888;
++
++ image.convertTo(targetFormat); // convert to a format mappable to QRhiTexture::Format
++ if (flipVertical)
++ image.flip(); // Flip vertically to the conventional Y-up orientation
++ return image;
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::loadQImage(const QString &inPath, qint32 flipVertical)
++ {
++ QImage image = loadImage(inPath, flipVertical);
++ if (image.isNull())
++ return nullptr;
++ QSSGLoadedTexture *retval = new QSSGLoadedTexture;
++ retval->width = image.width();
++ retval->height = image.height();
++ retval->components = image.pixelFormat().channelCount();
++ retval->image = image;
++ retval->data = (void *)retval->image.bits();
++ retval->dataSizeInBytes = image.sizeInBytes();
++ retval->setFormatFromComponents();
++ // #TODO: This is a very crude way detect color space
++ retval->isSRGB = image.colorSpace().transferFunction() != QColorSpace::TransferFunction::Linear;
++
++ return retval;
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::loadCompressedImage(const QString &inPath)
++ {
++ QSSGLoadedTexture *retval = nullptr;
++
++ // Open File
++ QFile imageFile(inPath);
++ if (!imageFile.open(QIODevice::ReadOnly)) {
++ qWarning() << "Could not open image file: " << inPath;
++ return retval;
++ }
++ auto reader = new QTextureFileReader(&imageFile, inPath);
++
++ if (!reader->canRead()) {
++ qWarning() << "Unable to read image file: " << inPath;
++ delete reader;
++ return retval;
++ }
++ retval = new QSSGLoadedTexture;
++ retval->textureFileData = reader->read();
++
++ // Fill out what makes sense, leave the rest at the default 0 and null.
++ retval->width = retval->textureFileData.size().width();
++ retval->height = retval->textureFileData.size().height();
++ auto glFormat = retval->textureFileData.glInternalFormat()
++ ? retval->textureFileData.glInternalFormat()
++ : retval->textureFileData.glFormat();
++ retval->format = fromGLtoTextureFormat(glFormat);
++
++ // SRGB8 ASTC formats are always sRGB
++ if ((retval->format.format >= QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_4x4) &&
++ (retval->format.format <= QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x12))
++ retval->isSRGB = true;
++
++ delete reader;
++ imageFile.close();
++
++ return retval;
++
++ }
++
++ namespace {
++ typedef unsigned char RGBE[4];
++ #define R 0
++ #define G 1
++ #define B 2
++ #define E 3
++
++ #define MINELEN 8 // minimum scanline length for encoding
++ #define MAXELEN 0x7fff // maximum scanline length for encoding
++
++ // Size in bytes of an image with the given geometry, or -1 if the result would
++ // overflow or exceed what QSSGLoadedTexture::dataSizeInBytes can describe. The
++ // dimensions come from image file headers, so they cannot be trusted.
++ qsizetype calculateDataSize(qsizetype width, qsizetype height, qsizetype depth, qsizetype bytesPerPixel)
++ {
++ if (width <= 0 || height <= 0 || depth < 0 || bytesPerPixel <= 0)
++ return -1;
++
++ qsizetype line = 0;
++ if (qMulOverflow(width, bytesPerPixel, &line))
++ return -1;
++ const qsizetype pitch = (line + 3) & ~qsizetype(3); // rows are 4-byte aligned
++
++ qsizetype dataSize = 0;
++ if (qMulOverflow(height, pitch, &dataSize))
++ return -1;
++ if (depth > 0 && qMulOverflow(dataSize, depth, &dataSize))
++ return -1;
++
++ if (quint64(dataSize) > quint64(std::numeric_limits<quint32>::max()))
++ return -1;
++
++ return dataSize;
++ }
++
++ float convertComponent(int exponent, int val)
++ {
++ float v = val / (256.0f);
++ float d = powf(2.0f, (float)exponent - 128.0f);
++ return v * d;
++ }
++
++ // Decodes one run length encoded scanline. Returns false, having written no
++ // more than w pixels, if the data is malformed; every run has to be bounded
++ // against the row, because the counts come from the file.
++ bool decrunchScanline(const char *&p, const char *pEnd, RGBE *scanline, int w)
++ {
++ if (pEnd - p < 4)
++ return false;
++
++ scanline[0][R] = *p++;
++ scanline[0][G] = *p++;
++ scanline[0][B] = *p++;
++ scanline[0][E] = *p++;
++
++ if (scanline[0][R] == 2 && scanline[0][G] == 2 && scanline[0][B] < 128) {
++ // new rle, the first pixel was a dummy
++ for (int channel = 0; channel < 4; ++channel) {
++ int x = 0;
++ while (x < w) {
++ if (p >= pEnd)
++ return false;
++ unsigned char c = *p++;
++ if (c > 128) { // run
++ const int repCount = c & 127;
++ if (p >= pEnd || repCount > w - x)
++ return false;
++ c = *p++;
++ for (int i = 0; i < repCount; ++i)
++ scanline[x++][channel] = c;
++ } else { // not a run
++ if (c > w - x || pEnd - p < c)
++ return false;
++ for (int i = 0; i < c; ++i)
++ scanline[x++][channel] = *p++;
++ }
++ }
++ }
++ } else {
++ // old rle
++ int bitshift = 0;
++ int x = 1;
++ while (x < w) {
++ if (pEnd - p < 4)
++ return false;
++ scanline[x][R] = *p++;
++ scanline[x][G] = *p++;
++ scanline[x][B] = *p++;
++ scanline[x][E] = *p++;
++
++ if (scanline[x][R] == 1 && scanline[x][G] == 1 && scanline[x][B] == 1) { // run
++ // A consecutive run shifts the count up by another byte, so
++ // beyond 24 the shift itself would be undefined, and the
++ // shifted count no longer fits in an int.
++ if (bitshift > 24 || scanline[x][E] > ((w - x) >> bitshift))
++ return false;
++ const int repCount = int(scanline[x][E]) << bitshift;
++ for (int i = 0; i < repCount; ++i, ++x)
++ memcpy(scanline[x], scanline[x - 1], 4);
++ bitshift += 8;
++ } else { // not a run
++ ++x;
++ bitshift = 0;
++ }
++ }
++ }
++
++ return true;
++ }
++
++ void decodeScanlineToTexture(RGBE *scanline, int width, void *outBuf, qsizetype offset, QSSGRenderTextureFormat inFormat)
++ {
++ quint8 *target = reinterpret_cast<quint8 *>(outBuf);
++ target += offset;
++
++ if (inFormat == QSSGRenderTextureFormat::RGBE8) {
++ memcpy(target, scanline, size_t(width) * 4);
++ } else {
++ float rgbaF32[4];
++ for (int i = 0; i < width; ++i) {
++ rgbaF32[R] = convertComponent(scanline[i][E], scanline[i][R]);
++ rgbaF32[G] = convertComponent(scanline[i][E], scanline[i][G]);
++ rgbaF32[B] = convertComponent(scanline[i][E], scanline[i][B]);
++ rgbaF32[3] = 1.0f;
++
++ inFormat.encodeToPixel(rgbaF32, target, i * inFormat.getSizeofFormat());
++ }
++ }
++ }
++
++ QSSGLoadedTexture *loadRadianceHdr(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat &format)
++ {
++ QSSGLoadedTexture *imageData = nullptr;
++
++ char sig[11];
++ source->seek(0);
++ if (source->read(sig, sizeof(sig)) == sizeof(sig) && !memcmp(sig, "#?RADIANCE\n", sizeof(sig))) {
++ QByteArray buf = source->readAll();
++ const char *p = buf.constData();
++ const char *pEnd = p + buf.size();
++
++ // Process lines until the empty one.
++ QByteArray line;
++ while (p < pEnd) {
++ char c = *p++;
++ if (c == '\n') {
++ if (line.isEmpty())
++ break;
++ if (line.startsWith(QByteArrayLiteral("FORMAT="))) {
++ const QByteArray format = line.mid(7).trimmed();
++ if (format != QByteArrayLiteral("32-bit_rle_rgbe")) {
++ qWarning("HDR format '%s' is not supported", format.constData());
++ return imageData;
++ }
++ }
++ line.clear();
++ } else {
++ line.append(c);
++ }
++ }
++ if (p == pEnd) {
++ qWarning("Malformed HDR image data at property strings");
++ return imageData;
++ }
++
++ // Get the resolution string.
++ while (p < pEnd) {
++ char c = *p++;
++ if (c == '\n')
++ break;
++ line.append(c);
++ }
++ if (p == pEnd) {
++ qWarning("Malformed HDR image data at resolution string");
++ return imageData;
++ }
++
++ int width = 0;
++ int height = 0;
++ // We only care about the standard orientation.
++ #ifdef Q_CC_MSVC
++ if (!sscanf_s(line.constData(), "-Y %d +X %d", &height, &width)) {
++ #else
++ if (!sscanf(line.constData(), "-Y %d +X %d", &height, &width)) {
++ #endif
++ qWarning("Unsupported HDR resolution string '%s'", line.constData());
++ return imageData;
++ }
++ if (width <= 0 || height <= 0 || width > 65536 || height > 65536) {
++ qWarning("Invalid HDR resolution %dx%d", width, height);
++ return imageData;
++ }
++
++ const int bytesPerPixel = format.getSizeofFormat();
++ const qsizetype dataSize = calculateDataSize(width, height, 0, bytesPerPixel);
++ if (dataSize < 0) {
++ qWarning("HDR image dimensions %dx%d are too large", width, height);
++ return imageData;
++ }
++ void *data = ::calloc(1, size_t(dataSize));
++ if (!data) {
++ qWarning("Failed to allocate %lld bytes for HDR image", qlonglong(dataSize));
++ return imageData;
++ }
++ imageData = new QSSGLoadedTexture;
++ imageData->dataSizeInBytes = quint32(dataSize);
++ imageData->data = data;
++ imageData->width = width;
++ imageData->height = height;
++ imageData->format = format;
++ imageData->components = format.getNumberOfComponent();
++ imageData->isSRGB = false;
++
++ // Allocate a scanline worth of RGBE data
++ RGBE *scanline = new RGBE[width];
++
++ // Note we are writing to the data buffer from bottom to top
++ // to correct for -Y orientation
++ for (int y = 0; y < height; ++y) {
++ const qsizetype byteOffset = qsizetype(height - 1 - y) * width * bytesPerPixel;
++ if (!decrunchScanline(p, pEnd, scanline, width)) {
++ qWarning("Malformed HDR scanline data at row %d of %d", y, height);
++ break;
++ }
++ decodeScanlineToTexture(scanline, width, imageData->data, byteOffset, format);
++ }
++
++ delete[] scanline;
++ }
++
++ return imageData;
++ }
++
++ QSSGLoadedTexture *loadExr(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat format)
++ {
++ QSSGLoadedTexture *imageData = nullptr;
++
++ char versionBuffer[tinyexr::kEXRVersionSize];
++ source->seek(0);
++ auto size = source->read(versionBuffer, tinyexr::kEXRVersionSize);
++ // Check if file is big enough
++ if (size != tinyexr::kEXRVersionSize)
++ return imageData;
++ // Try to load the Version
++ EXRVersion exrVersion;
++ if (ParseEXRVersionFromMemory(&exrVersion, reinterpret_cast<unsigned char *>(versionBuffer), tinyexr::kEXRVersionSize) != TINYEXR_SUCCESS)
++ return imageData;
++
++ // Check that the file is not a multipart file
++ if (exrVersion.multipart)
++ return imageData;
++
++ // If we get here, than this is an EXR file
++ source->seek(0);
++ QByteArray buf = source->readAll();
++ const char *err = nullptr;
++ // Header
++ EXRHeader exrHeader;
++ InitEXRHeader(&exrHeader);
++ if (ParseEXRHeaderFromMemory(&exrHeader,
++ &exrVersion,
++ reinterpret_cast<const unsigned char *>(buf.constData()),
++ buf.size(),
++ &err) != TINYEXR_SUCCESS) {
++ qWarning("Failed to parse EXR Header with error: '%s'", err);
++ FreeEXRErrorMessage(err);
++ return imageData;
++ }
++
++ // Make sure we get floats instead of half floats
++ for (int i = 0; i < exrHeader.num_channels; i++) {
++ if (exrHeader.pixel_types[i] == TINYEXR_PIXELTYPE_HALF)
++ exrHeader.requested_pixel_types[i] = TINYEXR_PIXELTYPE_FLOAT;
++ }
++
++ // Image
++ EXRImage exrImage;
++
++ InitEXRImage(&exrImage);
++ if (LoadEXRImageFromMemory(&exrImage,
++ &exrHeader,
++ reinterpret_cast<const unsigned char *>(buf.constData()),
++ buf.size(),
++ &err) != TINYEXR_SUCCESS) {
++ qWarning("Failed to load EXR Image with error: '%s'", err);
++ FreeEXRHeader(&exrHeader);
++ FreeEXRErrorMessage(err);
++ return imageData;
++ }
++
++ // Convert data
++ // RGBA
++ int idxR = -1;
++ int idxG = -1;
++ int idxB = -1;
++ int idxA = -1;
++ for (int c = 0; c < exrHeader.num_channels; c++) {
++ if (strcmp(exrHeader.channels[c].name, "R") == 0)
++ idxR = c;
++ else if (strcmp(exrHeader.channels[c].name, "G") == 0)
++ idxG = c;
++ else if (strcmp(exrHeader.channels[c].name, "B") == 0)
++ idxB = c;
++ else if (strcmp(exrHeader.channels[c].name, "A") == 0)
++ idxA = c;
++ }
++ const bool isSingleChannel = exrHeader.num_channels == 1;
++
++ // Anything but a lone channel is read as R, G and B, so all three have to be
++ // present. Without this a file whose channels are named something else (the
++ // luminance/chroma Y, BY, RY convention, for example) would index the plane
++ // array with -1.
++ if (!isSingleChannel && (idxR == -1 || idxG == -1 || idxB == -1)) {
++ qWarning("EXR image has no R, G and B channels");
++ FreeEXRImage(&exrImage);
++ FreeEXRHeader(&exrHeader);
++ return imageData;
++ }
++
++ // Setup Output container
++ const int bytesPerPixel = format.getSizeofFormat();
++ const qsizetype dataSize = calculateDataSize(exrImage.width, exrImage.height, 0, bytesPerPixel);
++ if (dataSize < 0) {
++ qWarning("EXR image dimensions %dx%d are too large", exrImage.width, exrImage.height);
++ FreeEXRImage(&exrImage);
++ FreeEXRHeader(&exrHeader);
++ return imageData;
++ }
++ void *data = ::malloc(size_t(dataSize));
++ if (!data) {
++ qWarning("Failed to allocate %lld bytes for EXR image", qlonglong(dataSize));
++ FreeEXRImage(&exrImage);
++ FreeEXRHeader(&exrHeader);
++ return imageData;
++ }
++ imageData = new QSSGLoadedTexture;
++ imageData->dataSizeInBytes = quint32(dataSize);
++ imageData->data = data;
++ imageData->width = exrImage.width;
++ imageData->height = exrImage.height;
++ imageData->format = format;
++ imageData->components = format.getNumberOfComponent();
++ imageData->isSRGB = false;
++
++ quint8 *target = reinterpret_cast<quint8 *>(imageData->data);
++
++ float rgbaF32[4];
++
++ if (exrHeader.tiled) {
++ for (int it = 0; it < exrImage.num_tiles; it++) {
++ for (int j = 0; j < exrHeader.tile_size_y; j++)
++ for (int i = 0; i < exrHeader.tile_size_x; i++) {
++ const int ii =
++ exrImage.tiles[it].offset_x * exrHeader.tile_size_x + i;
++ const int jj =
++ exrImage.tiles[it].offset_y * exrHeader.tile_size_y + j;
++ const int inverseJJ = std::abs(jj - (exrImage.height - 1));
++ const int idx = ii + inverseJJ * exrImage.width;
++
++ // out of region check.
++ if (ii >= exrImage.width) {
++ continue;
++ }
++ if (jj >= exrImage.height) {
++ continue;
++ }
++ const int srcIdx = i + j * exrHeader.tile_size_x;
++ unsigned char **src = exrImage.tiles[it].images;
++ if (isSingleChannel) {
++ rgbaF32[R] = reinterpret_cast<float **>(src)[0][srcIdx];
++ rgbaF32[G] = rgbaF32[R];
++ rgbaF32[B] = rgbaF32[R];
++ rgbaF32[3] = rgbaF32[R];
++ } else {
++ rgbaF32[R] = reinterpret_cast<float **>(src)[idxR][srcIdx];
++ rgbaF32[G] = reinterpret_cast<float **>(src)[idxG][srcIdx];
++ rgbaF32[B] = reinterpret_cast<float **>(src)[idxB][srcIdx];
++ if (idxA != -1)
++ rgbaF32[3] = reinterpret_cast<float **>(src)[idxA][srcIdx];
++ else
++ rgbaF32[3] = 1.0f;
++ }
++ format.encodeToPixel(rgbaF32, target + qsizetype(idx) * bytesPerPixel, 0);
++ }
++ }
++ } else {
++ int idx = 0;
++ for (int y = exrImage.height - 1; y >= 0; --y) {
++ for (int x = 0; x < exrImage.width; x++) {
++ const int i = y * exrImage.width + x;
++ if (isSingleChannel) {
++ rgbaF32[R] = reinterpret_cast<float **>(exrImage.images)[0][i];
++ rgbaF32[G] = rgbaF32[R];
++ rgbaF32[B] = rgbaF32[R];
++ rgbaF32[3] = rgbaF32[R];
++ } else {
++ rgbaF32[R] = reinterpret_cast<float **>(exrImage.images)[idxR][i];
++ rgbaF32[G] = reinterpret_cast<float **>(exrImage.images)[idxG][i];
++ rgbaF32[B] = reinterpret_cast<float **>(exrImage.images)[idxB][i];
++ if (idxA != -1)
++ rgbaF32[3] = reinterpret_cast<float **>(exrImage.images)[idxA][i];
++ else
++ rgbaF32[3] = 1.0f;
++ }
++ format.encodeToPixel(rgbaF32, target + qsizetype(idx) * bytesPerPixel, 0);
++ ++idx;
++ }
++ }
++ }
++
++ // Cleanup
++ FreeEXRImage(&exrImage);
++ FreeEXRHeader(&exrHeader);
++
++ return imageData;
++
++ }
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::loadLightmapImage(const QString &inPath, const QSSGRenderTextureFormat &inFormat, const QString &key)
++ {
++ if (auto stream = QSSGInputUtil::getStreamForFile(inPath))
++ return QSSGLightmapLoader::createTexture(stream, inFormat, key);
++ return nullptr;
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::loadHdrImage(const QSharedPointer<QIODevice> &source, const QSSGRenderTextureFormat &inFormat)
++ {
++ QSSGLoadedTexture *imageData = nullptr;
++ // We need to do a sanity check on the inFormat
++ QSSGRenderTextureFormat format = inFormat;
++ if (format.format == QSSGRenderTextureFormat::Unknown) {
++ // Loading HDR images for use outside of lightProbes will end up here
++ // The renderer doesn't understand RGBE8 textures outside of lightProbes
++ // So this needs to be a "real" format
++ // TODO: This is a fallback, but there is no way of telling here what formats are supported
++ format = QSSGRenderTextureFormat::RGBA16F;
++ }
++
++ // .hdr Files
++ imageData = loadRadianceHdr(source, format);
++
++ // .exr Files
++ if (!imageData)
++ imageData = loadExr(source, format);
++
++ return imageData;
++ }
++
++ // Size in bytes of tightly packed texture data, or -1 if the result would
++ // overflow or exceed what dataSizeInBytes can describe. The upload path assumes
++ // rows with no padding, so calculateDataSize()'s 4-byte aligned pitch must not
++ // be used here.
++ static qsizetype packedDataSize(const QSize &size, int depth, int bytesPerPixel)
++ {
++ if (size.width() <= 0 || size.height() <= 0 || depth < 0 || bytesPerPixel <= 0)
++ return -1;
++
++ qsizetype dataSize = bytesPerPixel;
++ if (qMulOverflow(dataSize, qsizetype(size.width()), &dataSize)
++ || qMulOverflow(dataSize, qsizetype(size.height()), &dataSize)
++ || qMulOverflow(dataSize, qsizetype(qMax(depth, 1)), &dataSize)) {
++ return -1;
++ }
++
++ if (quint64(dataSize) > quint64(std::numeric_limits<quint32>::max()))
++ return -1;
++
++ return dataSize;
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::loadTextureData(QSSGRenderTextureData *textureData)
++ {
++ QSSGLoadedTexture *imageData = new QSSGLoadedTexture;
++
++ if (!textureData->format().isCompressedTextureFormat()) {
++ const int bytesPerPixel = textureData->format().getSizeofFormat();
++ const qsizetype dataSize = packedDataSize(textureData->size(), textureData->depth(), bytesPerPixel);
++ if (dataSize < 0) {
++ qWarning("TextureData dimensions %dx%dx%d are not usable",
++ textureData->size().width(),
++ textureData->size().height(),
++ textureData->depth());
++ return imageData;
++ }
++ // The size only describes what the data is meant to be; the upload
++ // reads that many bytes from it either way.
++ if (dataSize > textureData->textureData().size()) {
++ qWarning("TextureData holds %lld bytes, but %dx%dx%d at %d bytes per pixel needs %lld",
++ qlonglong(textureData->textureData().size()),
++ textureData->size().width(),
++ textureData->size().height(),
++ textureData->depth(),
++ bytesPerPixel,
++ qlonglong(dataSize));
++ return imageData;
++ }
++ imageData->dataSizeInBytes = quint32(dataSize);
++ // We won't modifiy the data, but that is a nasty cast...
++ imageData->data = const_cast<void*>(reinterpret_cast<const void*>(textureData->textureData().data()));
++ imageData->width = textureData->size().width();
++ imageData->height = textureData->size().height();
++ imageData->depth = textureData->depth();
++ imageData->format = textureData->format();
++ imageData->components = textureData->format().getNumberOfComponent();
++ } else {
++ // Compressed Textures work a bit differently
++ // Fill out what makes sense, leave the rest at the default 0 and null.
++ const qsizetype dataSize = textureData->textureData().size();
++ if (quint64(dataSize) > quint64(std::numeric_limits<quint32>::max())) {
++ qWarning("TextureData holds %lld bytes, which exceeds the 4GB limit",
++ qlonglong(dataSize));
++ return imageData;
++ }
++ imageData->data = const_cast<void *>(reinterpret_cast<const void *>(textureData->textureData().data()));
++ imageData->dataSizeInBytes = quint32(dataSize);
++ // When we use depth we need to do slicing per layer for the uploads, but right now there it is non-trivial
++ // to determine the size of each "pixel" for compressed formats, so we don't support it for now.
++ // TODO: We need to force depth to 0 for now, as we don't support compressed 3D textures from texureData
++ imageData->width = textureData->size().width();
++ imageData->height = textureData->size().height();
++ imageData->format = textureData->format();
++ }
++
++ // #TODO: add an API to make this explicit
++ // For now we assume HDR formats are linear and everything else
++ // is sRGB, which is not ideal but so far this is only used by
++ // the environment mapper code
++ if (imageData->format == QSSGRenderTextureFormat::RGBE8 ||
++ imageData->format == QSSGRenderTextureFormat::RGBA16F ||
++ imageData->format == QSSGRenderTextureFormat::RGBA32F ||
++ imageData->format == QSSGRenderTextureFormat::BC6H)
++ imageData->isSRGB = false;
++ else
++ imageData->isSRGB = true;
++
++ return imageData;
++ }
++
++ namespace {
++
++ bool scanImageForAlpha(const void *inData, quint32 inWidth, quint32 inHeight, quint32 inPixelSizeInBytes, quint8 inAlphaSizeInBits)
++ {
++ const quint8 *rowPtr = reinterpret_cast<const quint8 *>(inData);
++ bool hasAlpha = false;
++ if (inAlphaSizeInBits == 0)
++ return hasAlpha;
++ if (inPixelSizeInBytes != 2 && inPixelSizeInBytes != 4) {
++ Q_ASSERT(false);
++ return false;
++ }
++ if (inAlphaSizeInBits > 8) {
++ Q_ASSERT(false);
++ return false;
++ }
++
++ quint32 alphaRightShift = inPixelSizeInBytes * 8 - inAlphaSizeInBits;
++ quint32 maxAlphaValue = (1 << inAlphaSizeInBits) - 1;
++
++ for (quint32 rowIdx = 0; rowIdx < inHeight && !hasAlpha; ++rowIdx) {
++ for (quint32 idx = 0; idx < inWidth && !hasAlpha; ++idx, rowPtr += inPixelSizeInBytes) {
++ quint32 pixelValue = 0;
++ if (inPixelSizeInBytes == 2)
++ pixelValue = *(reinterpret_cast<const quint16 *>(rowPtr));
++ else
++ pixelValue = *(reinterpret_cast<const quint32 *>(rowPtr));
++ pixelValue = pixelValue >> alphaRightShift;
++ if (pixelValue < maxAlphaValue)
++ hasAlpha = true;
++ }
++ }
++ return hasAlpha;
++ }
++ }
++
++ QSSGLoadedTexture::~QSSGLoadedTexture()
++ {
++ if (data && image.sizeInBytes() <= 0 && ownsData)
++ ::free(data);
++ }
++
++ bool QSSGLoadedTexture::scanForTransparency() const
++ {
++ switch (format.format) {
++ case QSSGRenderTextureFormat::SRGB8A8:
++ case QSSGRenderTextureFormat::RGBA8:
++ if (!data) // dds
++ return true;
++
++ return scanImageForAlpha(data, width, height, 4, 8);
++ // Scan the image.
++ case QSSGRenderTextureFormat::SRGB8:
++ case QSSGRenderTextureFormat::RGB8:
++ case QSSGRenderTextureFormat::RGBE8:
++ return false;
++ case QSSGRenderTextureFormat::RGB565:
++ return false;
++ case QSSGRenderTextureFormat::RGBA5551:
++ if (!data) { // dds
++ return true;
++ } else {
++ return scanImageForAlpha(data, width, height, 2, 1);
++ }
++ case QSSGRenderTextureFormat::Alpha8:
++ return true;
++ case QSSGRenderTextureFormat::R8:
++ case QSSGRenderTextureFormat::Luminance8:
++ case QSSGRenderTextureFormat::RG8:
++ return false;
++ case QSSGRenderTextureFormat::LuminanceAlpha8:
++ if (!data) // dds
++ return true;
++
++ return scanImageForAlpha(data, width, height, 2, 8);
++ case QSSGRenderTextureFormat::RGB_DXT1:
++ return false;
++ case QSSGRenderTextureFormat::RGBA_DXT3:
++ case QSSGRenderTextureFormat::RGBA_DXT1:
++ case QSSGRenderTextureFormat::RGBA_DXT5:
++ return false;
++ case QSSGRenderTextureFormat::RGB9E5:
++ return false;
++ case QSSGRenderTextureFormat::RG32F:
++ case QSSGRenderTextureFormat::RGB32F:
++ case QSSGRenderTextureFormat::RGBA16F:
++ case QSSGRenderTextureFormat::RGBA32F:
++ // TODO : For now, since IBL will be the main consumer, we'll just
++ // pretend there's no alpha. Need to do a proper scan down the line,
++ // but doing it for floats is a little different from integer scans.
++ return false;
++ default:
++ break;
++ }
++ Q_ASSERT(false);
++ return false;
++ }
++
++ static bool isCompatible(const QImage &img1, const QImage &img2)
++ {
++ if (img1.size() != img2.size())
++ return false;
++ if (img1.pixelFormat().channelCount() != img2.pixelFormat().channelCount())
++ return false;
++
++ return true;
++ }
++
++ static QSSGLoadedTexture *loadCubeMap(const QString &inPath, bool flipY)
++ {
++ QStringList fileNames;
++ if (inPath.contains(QStringLiteral("%p"))) {
++ fileNames.reserve(6);
++ const char *faces[6] = { "posx", "negx", "posy", "negy", "posz", "negz" };
++ for (const auto face : faces) {
++ QString fileName = inPath;
++ fileName.replace(QStringLiteral("%p"), QLatin1StringView(face));
++ fileNames << fileName;
++ }
++
++ } else if (inPath.contains(QStringLiteral(";"))) {
++ fileNames = inPath.split(QChar(u';'));
++ }
++ if (fileNames.size() != 6)
++ return nullptr; // TODO: allow sparse cube maps (with some faces missing)
++ std::unique_ptr<QTextureFileData> textureFileData = std::make_unique<QTextureFileData>(QTextureFileData::ImageMode);
++ textureFileData->setNumFaces(6);
++ textureFileData->setNumLevels(1);
++ textureFileData->setLogName(inPath.toUtf8());
++ QImage prevImage;
++ for (int i = 0; i < 6; ++i) {
++ QString searchName = fileNames[i];
++ QString filePath;
++ auto stream = QSSGInputUtil::getStreamForFile(searchName, true, &filePath);
++ if (!stream)
++ return nullptr;
++
++ QImage face = loadImage(filePath, !flipY); // Cube maps are flipped the other way
++ if (face.isNull() || (!prevImage.isNull() && !isCompatible(prevImage, face))) {
++ return nullptr;
++ }
++ textureFileData->setData(face, 0, i);
++ textureFileData->setSize(face.size());
++ prevImage = face;
++ }
++
++ const qsizetype dataSize = prevImage.sizeInBytes();
++ if (quint64(dataSize) > quint64(std::numeric_limits<quint32>::max())) {
++ qWarning("Cube map face %dx%d is too large to describe",
++ prevImage.width(),
++ prevImage.height());
++ return nullptr;
++ }
++
++ QSSGLoadedTexture *retval = new QSSGLoadedTexture;
++
++ retval->textureFileData = *textureFileData;
++
++ retval->width = prevImage.width();
++ retval->height = prevImage.height();
++ retval->components = prevImage.pixelFormat().channelCount();
++ retval->image = prevImage;
++ retval->data = (void *)retval->image.bits();
++ retval->dataSizeInBytes = quint32(dataSize);
++ retval->setFormatFromComponents();
++ // #TODO: This is a very crude way detect color space
++ retval->isSRGB = prevImage.colorSpace().transferFunction() != QColorSpace::TransferFunction::Linear;
++
++ return retval;
++ }
++
++ QSSGLoadedTexture *QSSGLoadedTexture::load(const QString &inPath,
++ const QSSGRenderTextureFormat &inFormat,
++ bool inFlipY)
++ {
++ if (inPath.isEmpty())
++ return nullptr;
++
++ QSSGLoadedTexture *theLoadedImage = nullptr;
++ QString fileName;
++ QSSGInputUtil::FileType fileType = QSSGInputUtil::UnknownFile;
++ QSharedPointer<QIODevice> theStream =
++ QSSGInputUtil::getStreamForTextureFile(inPath, true, &fileName, &fileType);
++
++ if (theStream) {
++ switch (fileType) {
++ case QSSGInputUtil::HdrFile:
++ // inFormat is a suggestion that's only relevant for HDR images
++ // (tells if we want want RGBA16F or RGBE-on-RGBA8)
++ theLoadedImage = loadHdrImage(theStream, inFormat);
++ break;
++ case QSSGInputUtil::TextureFile:
++ theLoadedImage = loadCompressedImage(fileName); // no choice but to ignore inFlipY here
++ break;
++ default:
++ theLoadedImage = loadQImage(fileName, inFlipY);
++ break;
++ }
++ } else {
++ // Check to see if we can find a cubemap
++ return loadCubeMap(inPath, inFlipY);
++ }
++ return theLoadedImage;
++ }
++
++ QT_END_NAMESPACE