// Minimal reproducer for the SIGSEGV in libnvidia-glcore.so.1 seen in
// plasmashell under Qt's threaded render loop (KDE bug 526457). Plain Qt Quick,
// no Plasma or KDE modules.
//
// What it does:
//  - one window with a grid of 198x92 tiles; each has a textured "pressed"
//    frame whose opacity fades when the tile's state flips (OpacityAnimator),
//    so the render thread re-uploads a dynamic vertex buffer every frame;
//  - at each state flip (about 3-4 per second, in bursts) a new translucent
//    popup window is shown, with its own render thread and GL context, and
//    destroyed a few seconds later.
// The crash is a SIGSEGV at libnvidia-glcore.so.1 + 0xa2abc0 (595.104.02) in the
// main window's render thread, under QSGBatchRenderer::Renderer::unmap <-
// uploadBatch, while the GUI thread is in glXGetFBConfigAttrib for a new popup
// (QOffscreenSurface::create on its first expose). Findings (2026-10-26):
//  - crashes in 10-115 s with the defaults (7 of 7 runs, with or without LAYER);
//  - needs the popup windows: no crash in 3 min with NO_POPUPS=1, or with only
//    the GLX fbconfig queries / a QOffscreenSurface per flip (POPUP_MODE);
//  - creating them is enough: it also crashes when no popup is ever destroyed;
//  - needs a high enough popup rate: no crash in 3 min at 1 popup/s;
//  - no layer needed (LAYER=1 adds one; plasmashell crashed inside layers).
//
// Environment knobs (defaults in brackets):
//   TILES=n          number of tiles [40]
//   COLUMNS=n        grid columns [11]
//   TRIGGER_MS=n     mean interval between state flips [300], +-50% jitter
//   BURST_PCT=n      chance (percent) that a flip starts a burst [60]
//   BURST_N=n        flips per burst [6]
//   BURST_GAP_MS=n   interval between flips inside a burst [200]
//   POPUP_LIFE_MS=n  how long a popup stays [5000]
//   NO_POPUPS=1      flip tiles without showing popups (control)
//   POPUP_MODE=x     what each flip does instead of a full popup window [window]:
//                    surface = create and destroy a QOffscreenSurface (Qt's
//                              fallback surface for a new window, which queries
//                              GLX fbconfigs on the GUI thread);
//                    glx     = call glXGetFBConfigs and glXGetFBConfigAttrib for
//                              all configs directly, nothing else
//   LAYER=1          wrap the grid in a layer, as in the plasmashell crashes
//   TILE_OPACITY=x   opacity of the tiles [0.5], like a disabled Plasma button
//   VISIBLE=1        show the windows on screen; by default they bypass the
//                    window manager and sit outside the screen area, so the
//                    program can run without disturbing the desktop
//   REPRO_LOOP=x     render loop [threaded]; QSG_RENDER_LOOP is ignored
//
// Build: c++ -std=c++17 -O1 -g -fPIC -o glxrepro-min main.cpp \
//          $(pkg-config --cflags --libs Qt6Quick Qt6Qml gl x11)
// Run:   ./glxrepro-min      (crashes typically within two minutes)
#include <QDir>
#include <QElapsedTimer>
#include <QGuiApplication>
#include <QImage>
#include <QOffscreenSurface>
#include <QPainter>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQuickWindow>
#include <QRandomGenerator>
#include <QTimer>
#include <cstdio>
#include <GL/glx.h>

static int envInt(const char *name, int def)
{
    const QByteArray v = qgetenv(name);
    bool ok = false;
    const int n = v.toInt(&ok);
    return ok ? n : def;
}

static const char *mainQml = R"QML(
import QtQuick
import QtQuick.Window

Window {
    id: win
    property int tiles: 40
    property int columns: 11
    property bool useLayer: false
    property real tileOpacity: 0.5
    property bool offscreen: true
    property url frameSource
    property int frames: 0
    onFrameSwapped: frames++

    width: 202 * columns + 8
    height: 96 * Math.ceil(tiles / columns) + 8
    visible: true
    color: "#1d2a35"
    flags: offscreen ? (Qt.Window | Qt.BypassWindowManagerHint | Qt.FramelessWindowHint) : Qt.Window
    x: offscreen ? -width - 1000 : 100
    y: offscreen ? 2000 : 100

    ListModel { id: tileModel }
    Component.onCompleted: {
        for (let i = 0; i < tiles; ++i)
            tileModel.append({ active: false })
    }

    function flip() {
        const i = Math.floor(Math.random() * tileModel.count)
        tileModel.setProperty(i, "active", !tileModel.get(i).active)
    }

    Item {
        anchors.fill: parent
        anchors.margins: 4
        layer.enabled: win.useLayer

        Grid {
            columns: win.columns
            spacing: 4
            Repeater {
                model: tileModel
                Item {
                    required property bool active
                    width: 198
                    height: 92
                    opacity: win.tileOpacity

                    Rectangle {
                        anchors.fill: parent
                        radius: 4
                        color: "#2b3c4a"
                        border.color: "#4a6070"
                    }
                    // Like Plasma's pressed frame: a textured item faded in and out.
                    Image {
                        anchors.fill: parent
                        source: win.frameSource
                        opacity: active ? 1 : 0
                        Behavior on opacity { OpacityAnimator { duration: 250 } }
                    }
                    Text {
                        anchors.centerIn: parent
                        color: "white"
                        text: active ? "on" : "off"
                    }
                }
            }
        }
    }
}
)QML";

static const char *popupQml = R"QML(
import QtQuick
import QtQuick.Window

Window {
    property bool offscreen: true
    width: 360; height: 100
    flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
           | (offscreen ? Qt.BypassWindowManagerHint : 0)
    color: "transparent"
    visible: true
    Rectangle {
        anchors.fill: parent
        radius: 8
        color: "#e0303840"
        opacity: 0.0
        NumberAnimation on opacity { to: 1.0; duration: 150 }
        Text { anchors.centerIn: parent; color: "white"; text: "Notification"; font.pixelSize: 18 }
    }
}
)QML";

int main(int argc, char **argv)
{
    const QByteArray loop = qgetenv("REPRO_LOOP");
    qputenv("QSG_RENDER_LOOP", loop.isEmpty() ? QByteArray("threaded") : loop);
    QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);

    QGuiApplication app(argc, argv);
    QQmlEngine engine;

    const int tiles = envInt("TILES", 40);
    const int columns = envInt("COLUMNS", 11);
    const int triggerMs = qMax(envInt("TRIGGER_MS", 300), 20);
    const int burstPct = envInt("BURST_PCT", 60);
    const int burstN = qMax(envInt("BURST_N", 6), 1);
    const int burstGapMs = envInt("BURST_GAP_MS", 200);
    const int popupLifeMs = envInt("POPUP_LIFE_MS", 5000);
    const bool popups = !qEnvironmentVariableIsSet("NO_POPUPS");
    const QByteArray mode = qEnvironmentVariableIsSet("POPUP_MODE") ? qgetenv("POPUP_MODE")
                                                                     : QByteArray("window");
    if (mode != "window" && mode != "surface" && mode != "glx") {
        std::fprintf(stderr, "POPUP_MODE must be window, surface or glx\n");
        return 1;
    }
    const bool useLayer = qEnvironmentVariableIsSet("LAYER");
    const bool offscreen = !qEnvironmentVariableIsSet("VISIBLE");
    bool ok = false;
    double tileOpacity = qgetenv("TILE_OPACITY").toDouble(&ok);
    if (!ok)
        tileOpacity = 0.5;

    std::fprintf(stderr, "loop=%s TILES=%d COLUMNS=%d TRIGGER_MS=%d BURST_PCT=%d BURST_N=%d "
                 "BURST_GAP_MS=%d POPUP_LIFE_MS=%d popups=%d POPUP_MODE=%s layer=%d TILE_OPACITY=%.2f offscreen=%d\n",
                 qgetenv("QSG_RENDER_LOOP").constData(), tiles, columns, triggerMs, burstPct,
                 burstN, burstGapMs, popupLifeMs, popups, mode.constData(), useLayer, tileOpacity, offscreen);

    // The pressed frame's texture.
    const QString base = QDir::currentPath() + QLatin1Char('/');
    QImage frame(198, 92, QImage::Format_ARGB32_Premultiplied);
    frame.fill(Qt::transparent);
    {
        QPainter p(&frame);
        p.setRenderHint(QPainter::Antialiasing);
        p.setPen(QPen(QColor(61, 174, 233), 2));
        p.setBrush(QColor(61, 174, 233, 90));
        p.drawRoundedRect(QRectF(1, 1, 196, 90), 4, 4);
    }
    const QString framePath = QDir::temp().filePath(QStringLiteral("glxrepro-min-frame.png"));
    if (!frame.save(framePath)) {
        std::fprintf(stderr, "cannot write %s\n", qPrintable(framePath));
        return 1;
    }

    // Local-file URLs make setData() compile synchronously.
    QQmlComponent mainComp(&engine);
    mainComp.setData(mainQml, QUrl::fromLocalFile(base + "main.qml"));
    QQmlComponent popupComp(&engine);
    popupComp.setData(popupQml, QUrl::fromLocalFile(base + "popup.qml"));
    if (!mainComp.isReady() || !popupComp.isReady()) {
        std::fprintf(stderr, "%s%s\n", qPrintable(mainComp.errorString()),
                     qPrintable(popupComp.errorString()));
        return 1;
    }

    QObject *win = mainComp.createWithInitialProperties({
            {"tiles", tiles}, {"columns", columns}, {"useLayer", useLayer},
            {"tileOpacity", tileOpacity}, {"offscreen", offscreen},
            {"frameSource", QUrl::fromLocalFile(framePath)}});
    if (!qobject_cast<QQuickWindow *>(win)) {
        std::fprintf(stderr, "main: %s\n", qPrintable(mainComp.errorString()));
        return 1;
    }

    long flips = 0, popupsCreated = 0;
    Display *dpy = nullptr;
    if (auto *x11 = app.nativeInterface<QNativeInterface::QX11Application>())
        dpy = x11->display();
    if (mode == "glx" && !dpy) {
        std::fprintf(stderr, "no X11 display\n");
        return 1;
    }
    // What QOffscreenSurface::create() does with GLX for a new window's format,
    // reduced to the fbconfig queries.
    auto queryFbConfigs = [&] {
        int n = 0;
        GLXFBConfig *configs = glXGetFBConfigs(dpy, DefaultScreen(dpy), &n);
        static const int attribs[] = { GLX_RED_SIZE, GLX_GREEN_SIZE, GLX_BLUE_SIZE,
                                       GLX_ALPHA_SIZE, GLX_DEPTH_SIZE, GLX_STENCIL_SIZE,
                                       GLX_DOUBLEBUFFER, GLX_SAMPLES, GLX_DRAWABLE_TYPE,
                                       GLX_RENDER_TYPE, GLX_VISUAL_ID };
        int value;
        for (int i = 0; i < n; ++i)
            for (int a : attribs)
                glXGetFBConfigAttrib(dpy, configs[i], a, &value);
        XFree(configs);
    };
    auto showPopup = [&] {
        if (mode == "surface") {
            QOffscreenSurface surface;
            surface.setFormat(QSurfaceFormat::defaultFormat());
            surface.create();
            ++popupsCreated;
            return;
        }
        if (mode == "glx") {
            queryFbConfigs();
            ++popupsCreated;
            return;
        }
        auto *p = qobject_cast<QQuickWindow *>(
                popupComp.createWithInitialProperties({{"offscreen", offscreen}}));
        if (!p) {
            std::fprintf(stderr, "popup: %s\n", qPrintable(popupComp.errorString()));
            return;
        }
        p->setPosition(offscreen ? -3000 : 1500, (offscreen ? 2000 : 80) + (popupsCreated % 6) * 110);
        ++popupsCreated;
        QTimer::singleShot(popupLifeMs, p, [p] {
            p->destroy();
            p->deleteLater();
        });
    };

    auto stateChange = [&] {
        QMetaObject::invokeMethod(win, "flip");
        ++flips;
        if (popups)
            showPopup();
    };
    QTimer trigger;
    trigger.setSingleShot(true);
    QObject::connect(&trigger, &QTimer::timeout, [&] {
        stateChange();
        int next = triggerMs / 2 + QRandomGenerator::global()->bounded(triggerMs);
        if (QRandomGenerator::global()->bounded(100) < burstPct) {
            for (int i = 1; i < burstN; ++i)
                QTimer::singleShot(i * burstGapMs, stateChange);
            next += burstN * burstGapMs;
        }
        trigger.start(next);
    });
    trigger.start(triggerMs);

    QElapsedTimer clock;
    clock.start();
    QTimer stats;
    QObject::connect(&stats, &QTimer::timeout, [&] {
        std::fprintf(stderr, "[%6llds] flips %ld, popups %ld, frames %lld\n",
                     static_cast<long long>(clock.elapsed() / 1000), flips, popupsCreated,
                     win->property("frames").toLongLong());
    });
    stats.start(10000);

    return app.exec();
}
