// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default


#include "qcrhicolorglyphcache_p.h"
#include <QtGui/private/qrawfont_p.h>

QT_BEGIN_NAMESPACE

// Padding between glyphs so linear filtering never samples a neighbour.
static constexpr int QC_COLOR_GLYPH_PADDING = 1;

QCRhiColorGlyphCache::QCRhiColorGlyphCache(QRhi *rhi)
    : m_rhi(rhi)
{
    m_batch = rhi->nextResourceUpdateBatch();
}

QCRhiColorGlyphCache::~QCRhiColorGlyphCache()
{
    if (m_batch)
        m_batch->release();
    if (m_atlas)
        m_atlas->deleteLater();
    delete m_allocator;
}

bool QCRhiColorGlyphCache::useTextureResizeWorkaround() const
{
    static bool useWorkaround = m_rhi->backend() == QRhi::OpenGLES2;
    return useWorkaround;
}

void QCRhiColorGlyphCache::ensureAtlas()
{
    if (m_atlas)
        return;

    m_maxAtlasSize = qMin(2048, m_rhi->resourceLimit(QRhi::TextureSizeMax));
    m_atlasSize = qMin(256, m_maxAtlasSize);
    m_atlas = m_rhi->newTexture(QRhiTexture::RGBA8, QSize(m_atlasSize, m_atlasSize), 1,
                                QRhiTexture::UsedAsTransferSource);
    m_atlas->setName("QCColorGlyphAtlas");
    if (!m_atlas->create()) {
        qWarning("Failed to create color glyph atlas");
        delete m_atlas;
        m_atlas = nullptr;
        return;
    }

    const QByteArray zeroBuf(m_atlasSize * m_atlasSize * 4, 0);
    QRhiTextureSubresourceUploadDescription zeroSubres(zeroBuf.constData(), zeroBuf.size());
    zeroSubres.setSourceSize(QSize(m_atlasSize, m_atlasSize));
    m_batch->uploadTexture(m_atlas, QRhiTextureUploadEntry(0, 0, zeroSubres));

    m_allocator = new QCAreaAllocator(QSize(m_maxAtlasSize, m_maxAtlasSize));

    if (useTextureResizeWorkaround()) {
        m_atlasImage = QImage(m_atlasSize, m_atlasSize, QImage::Format_RGBA8888_Premultiplied);
        m_atlasImage.fill(Qt::transparent);
    }
}

void QCRhiColorGlyphCache::growAtlas(int requiredSize)
{
    int newSize = m_atlasSize;
    while (newSize < requiredSize)
        newSize *= 2;
    newSize = qMin(newSize, m_maxAtlasSize);
    if (newSize == m_atlasSize)
        return;

    QRhiTexture *newAtlas = m_rhi->newTexture(QRhiTexture::RGBA8, QSize(newSize, newSize), 1,
                                              QRhiTexture::UsedAsTransferSource);
    newAtlas->setName("QCColorGlyphAtlas");
    if (!newAtlas->create()) {
        qWarning("Failed to grow color glyph atlas");
        delete newAtlas;
        return;
    }

    if (useTextureResizeWorkaround()) {
        // Enlarge the CPU shadow first (the area outside the old image is
        // filled with transparent black) and upload it in full, so the region
        // beyond the old size does not stay undefined in the new texture.
        m_atlasImage = m_atlasImage.copy(0, 0, newSize, newSize);
        QRhiTextureSubresourceUploadDescription subres(m_atlasImage.constBits(),
                                                       m_atlasImage.sizeInBytes());
        subres.setSourceSize(QSize(newSize, newSize));
        m_batch->uploadTexture(newAtlas, QRhiTextureUploadEntry(0, 0, subres));
    } else {
        const QByteArray zeroBuf(newSize * newSize * 4, 0);
        QRhiTextureSubresourceUploadDescription zeroSubres(zeroBuf.constData(), zeroBuf.size());
        zeroSubres.setSourceSize(QSize(newSize, newSize));
        m_batch->uploadTexture(newAtlas, QRhiTextureUploadEntry(0, 0, zeroSubres));
        m_batch->copyTexture(newAtlas, m_atlas);
    }

    m_atlas->deleteLater();
    m_atlas = newAtlas;
    m_atlasSize = newSize;
}

static inline QRect allocationRect(const QRect &atlasRect)
{
    return QRect(atlasRect.x() - QC_COLOR_GLYPH_PADDING,
                 atlasRect.y() - QC_COLOR_GLYPH_PADDING,
                 atlasRect.width() + QC_COLOR_GLYPH_PADDING * 2,
                 atlasRect.height() + QC_COLOR_GLYPH_PADDING * 2);
}

void QCRhiColorGlyphCache::referenceGlyph(const GlyphKey &key, GlyphData &gd)
{
    // Count one use per frame, so a glyph drawn many times in a frame is not
    // weighted more heavily than one drawn once (matches the SDF cache).
    if (!m_referencedThisFrame.contains(key)) {
        ++gd.ref;
        m_referencedThisFrame.insert(key);
    }
    m_unusedGlyphs.remove(key);
}

// Free atlas space held by unused glyphs until allocSize fits (or none are
// left to evict). Returns the resulting allocation, or a null rect.
QRect QCRhiColorGlyphCache::evictUntilAllocated(const QSize &allocSize)
{
    QRect alloc;
    while (alloc.isNull() && !m_unusedGlyphs.isEmpty()) {
        const GlyphKey unused = *m_unusedGlyphs.constBegin();
        auto it = m_glyphs.constFind(unused);
        if (it != m_glyphs.constEnd() && it->valid)
            m_allocator->deallocate(allocationRect(it->atlasRect));
        m_unusedGlyphs.remove(unused);
        m_glyphs.remove(unused);
        alloc = m_allocator->allocate(allocSize);
    }
    return alloc;
}

const QCRhiColorGlyphCache::GlyphData &
QCRhiColorGlyphCache::ensureGlyph(const GlyphKey &key, QFontEngine *fe,
                                  const QColor &color, const QTransform &rasterTransform)
{
    auto it = m_glyphs.find(key);
    if (it != m_glyphs.end()) {
        if (it->valid)
            referenceGlyph(key, it.value());
        return it.value();
    }

    GlyphData gd;

    ensureAtlas();
    if (!m_atlas)
        return m_invalidGlyph;

    // The bitmap is already in Format_ARGB32_Premultiplied for color fonts.
    QImage bitmap = fe->bitmapForGlyph(key.glyph, QFixedPoint(), rasterTransform, color);
    if (bitmap.isNull())
        return *m_glyphs.insert(key, gd); // permanently invalid: no color bitmap for this glyph

    if (bitmap.format() != QImage::Format_RGBA8888_Premultiplied)
        bitmap = bitmap.convertToFormat(QImage::Format_RGBA8888_Premultiplied);

    const QSize glyphSize = bitmap.size();
    const QSize allocSize(glyphSize.width() + QC_COLOR_GLYPH_PADDING * 2,
                          glyphSize.height() + QC_COLOR_GLYPH_PADDING * 2);
    QRect alloc = m_allocator->allocate(allocSize);
    if (alloc.isNull())
        alloc = evictUntilAllocated(allocSize);
    if (alloc.isNull()) {
        qWarning("Color glyph atlas full; emoji glyph dropped");
        return m_invalidGlyph;
    }

    // The allocator hands out positions across the full (max) coordinate space;
    // grow the physical texture to cover this allocation before uploading into
    // it. The allocation is bounded by m_maxAtlasSize, so the grow always
    // succeeds in reaching it.
    const int required = qMax(alloc.x() + alloc.width(), alloc.y() + alloc.height());
    if (required > m_atlasSize)
        growAtlas(required);

    const QPoint dst = alloc.topLeft() + QPoint(QC_COLOR_GLYPH_PADDING, QC_COLOR_GLYPH_PADDING);

    QRhiTextureSubresourceUploadDescription subres(bitmap.constBits(), bitmap.sizeInBytes());
    subres.setSourceSize(glyphSize);
    subres.setDestinationTopLeft(dst);
    m_batch->uploadTexture(m_atlas, QRhiTextureUploadEntry(0, 0, subres));

    if (useTextureResizeWorkaround()) {
        const int bytesPerRow = glyphSize.width() * 4;
        for (int row = 0; row < glyphSize.height(); ++row) {
            memcpy(m_atlasImage.scanLine(dst.y() + row) + dst.x() * 4,
                   bitmap.constScanLine(row),
                   bytesPerRow);
        }
    }

    // Bearing: where the bitmap's top-left sits relative to the glyph pen
    // position on the baseline (alphaMapBoundingBox returns this offset).
    const glyph_metrics_t gm =
        fe->alphaMapBoundingBox(key.glyph, QFixedPoint(), rasterTransform,
                                QFontEngine::Format_ARGB);

    gd.atlasRect = QRect(dst, glyphSize);
    gd.bearing = QPointF(gm.x.toReal(), gm.y.toReal());
    gd.valid = true;
    GlyphData &stored = *m_glyphs.insert(key, gd);
    referenceGlyph(key, stored);
    return stored;
}

void QCRhiColorGlyphCache::releaseGlyphs(const QSet<GlyphKey> &glyphs)
{
    for (const GlyphKey &key : glyphs) {
        auto it = m_glyphs.find(key);
        if (it == m_glyphs.end())
            continue;
        if (it->ref > 0 && --it->ref == 0)
            m_unusedGlyphs.insert(key);
    }
}

void QCRhiColorGlyphCache::optimizeAfterRendering()
{
    releaseGlyphs(m_referencedPrevFrame);
    m_referencedPrevFrame.swap(m_referencedThisFrame);
    m_referencedThisFrame.clear();
}

void QCRhiColorGlyphCache::addGlyphRun(const QPointF &glyphPos,
                                       const QGlyphRun &run,
                                       const QColor &color,
                                       const QTransform &transform,
                                       float devicePixelRatio,
                                       QCRhiDistanceFieldGlyphCache::VertexList *verts,
                                       QCRhiDistanceFieldGlyphCache::IndexList *indices)
{
    const QRawFont rawFont = run.rawFont();
    QFontEngine *fe = QRawFontPrivate::get(rawFont)->fontEngine;
    if (!fe || !fe->isColorFont())
        return;

    const QList<quint32> glyphIndexes = run.glyphIndexes();
    const QList<QPointF> positions = run.positions();
    const int count = qMin(glyphIndexes.size(), positions.size());

    const QFontEngine::FaceId faceId = fe->faceId();
    const int pixelSize = qRound(rawFont.pixelSize());
    // The color participates in the key because COLR fonts can have layers
    // painted in the text ("foreground") color.
    const QRgb rgba = color.rgba();

    // Rasterize at the device pixel ratio so the bitmaps match the physical
    // resolution of the render target; the quads below stay in logical
    // coordinates and the projection scales them back up. The painter
    // transform is still only applied to the quad corners (consistent with
    // the SDF path), so a scaling transform does reduce sharpness.
    qreal scale = devicePixelRatio > 0.0f ? devicePixelRatio : 1.0;
    QTransform rasterTransform = QTransform::fromScale(scale, scale);
    if (!fe->supportsTransformation(rasterTransform)) {
        scale = 1.0;
        rasterTransform = QTransform();
    }

    for (int i = 0; i < count; ++i) {
        const GlyphKey key{ faceId, glyphIndexes.at(i), pixelSize, float(scale), rgba };
        const GlyphData &gd = ensureGlyph(key, fe, color, rasterTransform);
        if (!gd.valid)
            continue;

        // Pen position on the baseline for this glyph, then offset by the
        // bearing. The bitmap and its metrics are in device pixels; divide by
        // the rasterization scale to get back to logical coordinates.
        const QPointF pen = glyphPos + positions.at(i);
        const qreal x = pen.x() + gd.bearing.x() / scale;
        const qreal y = pen.y() + gd.bearing.y() / scale;
        const qreal w = gd.atlasRect.width() / scale;
        const qreal h = gd.atlasRect.height() / scale;

        QPointF p1(x,     y);
        QPointF p2(x + w, y);
        QPointF p3(x,     y + h);
        QPointF p4(x + w, y + h);
        if (!transform.isIdentity()) {
            p1 = transform.map(p1);
            p2 = transform.map(p2);
            p3 = transform.map(p3);
            p4 = transform.map(p4);
        }

        // Texture coords in atlas texels (shader divides by textureSize()).
        const float tx1 = gd.atlasRect.x();
        const float ty1 = gd.atlasRect.y();
        const float tx2 = tx1 + float(gd.atlasRect.width());
        const float ty2 = ty1 + float(gd.atlasRect.height());

        const uint32_t base = uint32_t(verts->size());
        QCRhiDistanceFieldGlyphCache::TexturedPoint2D v;
        v.set(float(p1.x()), float(p1.y()), tx1, ty1); verts->append(v);
        v.set(float(p2.x()), float(p2.y()), tx2, ty1); verts->append(v);
        v.set(float(p3.x()), float(p3.y()), tx1, ty2); verts->append(v);
        v.set(float(p4.x()), float(p4.y()), tx2, ty2); verts->append(v);

        indices->append(base + 0);
        indices->append(base + 2);
        indices->append(base + 3);
        indices->append(base + 3);
        indices->append(base + 1);
        indices->append(base + 0);
    }
}

void QCRhiColorGlyphCache::commitResourceUpdate(QRhiResourceUpdateBatch *batch)
{
    batch->merge(m_batch);
    m_batch->release();
    m_batch = m_rhi->nextResourceUpdateBatch();
}

QT_END_NAMESPACE
