////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2026 OVITO GmbH, Germany
//
//  This file is part of OVITO (Open Visualization Tool).
//
//  OVITO is free software; you can redistribute it and/or modify it either under the
//  terms of the GNU General Public License version 3 as published by the Free Software
//  Foundation (the "GPL") or, at your option, under the terms of the MIT License.
//  If you do not alter this notice, a recipient may use your version of this
//  file under either the GPL or the MIT License.
//
//  You should have received a copy of the GPL along with this program in a
//  file LICENSE.GPL.txt.  You should have received a copy of the MIT License along
//  with this program in a file LICENSE.MIT.txt
//
//  This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
//  either express or implied. See the GPL or the MIT License for the specific language
//  governing rights and limitations.
//
////////////////////////////////////////////////////////////////////////////////////////

#include <ovito/core/Core.h>
#include <ovito/core/app/Application.h>
#include <ovito/core/rendering/RenderSettings.h>
#include <ovito/core/rendering/ColorCodingGradient.h>
#include <ovito/core/rendering/FrameGraph.h>
#include <ovito/core/rendering/ColorMapHelper.h>
#include "OpenGLRenderingJob.h"
#include "OpenGLRenderBuffer.h"
#include "OpenGLHelpers.h"
#include "OpenGLShaderHelper.h"
#include "OpenGLTexture.h"

#include <QOffscreenSurface>
#include <QSurface>
#include <QWindow>
#include <QScreen>
#include <QOpenGLFunctions_3_0>
#include <QOpenGLVersionFunctionsFactory>
#include <QOpenGLTexture>
#include <QOpenGLDebugLogger>

namespace Ovito {

IMPLEMENT_ABSTRACT_OVITO_CLASS(OpenGLRenderingJob);

/******************************************************************************
* Constructor.
******************************************************************************/
void OpenGLRenderingJob::initializeObject(ObjectInitializationFlags flags, std::shared_ptr<RendererResourceCache> visCache, OORef<const OpenGLRenderer> sceneRenderer, int supersamplingLevel)
{
    RenderingJob::initializeObject(flags);

    _visCache = std::move(visCache);
    _sceneRenderer = std::move(sceneRenderer);
    _supersamplingLevel = std::max(1, supersamplingLevel);
}

/******************************************************************************
* Called when this object is being destroyed.
******************************************************************************/
void OpenGLRenderingJob::aboutToBeDeleted()
{
    RenderingJob::aboutToBeDeleted();

    // Release all cached OpenGL resources from the last frame rendered.
    // This may require an active GL context.
    if(_currentResourceFrame) {
        OpenGLContextRestore contextRestore = activateContext();
        _currentResourceFrame = {};
    }
}

/******************************************************************************
* Creates a new abstract target frame buffer for rendering into.
******************************************************************************/
OORef<RenderBuffer> OpenGLRenderingJob::createOffscreenRenderBuffer(const QSize& deviceIndependentSize)
{
    // Creating an OpenGL framebuffer requires an active OpenGL context.
    OpenGLContextRestore contextRestore = activateContext();

    return OORef<OpenGLRenderBuffer>::create(this, deviceIndependentSize);
}

/******************************************************************************
* Renders an image of the given frame graph. The image is first rendered into the given device-specific render buffer,
* and then optionally copied into the given output frame buffer (may be null).
******************************************************************************/
SCFuture<void> OpenGLRenderingJob::renderFrame(std::shared_ptr<const FrameGraph> frameGraph, OORef<RenderBuffer> renderBuffer, std::shared_ptr<FrameBuffer> frameBuffer, TaskProgress& progress)
{
    return renderFrame(std::move(frameGraph), static_object_cast<OpenGLRenderBuffer>(std::move(renderBuffer)), std::move(frameBuffer), nullptr);
}

/******************************************************************************
* Renders an image of the given frame graph into the given target frame buffer.
******************************************************************************/
SCFuture<void> OpenGLRenderingJob::renderFrame(std::shared_ptr<const FrameGraph> frameGraph, OORef<OpenGLRenderBuffer> renderBuffer, std::shared_ptr<FrameBuffer> frameBuffer, std::shared_ptr<OpenGLPickingMap> pickingMap)
{
    OVITO_ASSERT(this_task::ui());

    // OpenGL rendering requires a Qt GUI application.
    if(!qobject_cast<QGuiApplication*>(QCoreApplication::instance())) {
        throw RendererException(
            tr("OVITO's OpenGLRenderer cannot be used in headless mode, that is if the application is running without access to a desktop graphics "
               "environment. "
               "Please use a different rendering backend or see "
               "https://docs.ovito.org/python/modules/ovito_vis.html#ovito.vis.OpenGLRenderer for instructions "
               "on how to enable OpenGL rendering in Python scripts."));
    }

    // Reset warnings of the render buffer.
    renderBuffer->resetIssues();

    // Rendering requires an active GL context.
    OpenGLContextRestore contextRestore = activateContext();
    _glcontext = QOpenGLContext::currentContext();
    if(!_glcontext)
        throw RendererException(tr("Cannot render scene: There is no active OpenGL context"));

    // A picking render pass must always be done into an offscreen framebuffer.
    OVITO_ASSERT(!pickingMap || renderBuffer->framebufferObject().has_value());

    // Bind offscreen OpenGL framebuffer for rendering.
    if(renderBuffer->framebufferObject().has_value() && !renderBuffer->framebufferObject()->bind())
        throw RendererException(tr("Failed to bind OpenGL framebuffer object for offscreen rendering."));

    // Store physical OpenGL framebuffer size.
    _framebufferSize = renderBuffer->size();

    // Adopt OIT mode from scene renderer instance.
    if(_sceneRenderer)
        _orderIndependentTransparency = _sceneRenderer->orderIndependentTransparency();

    // Store pointers to important objects internally.
    _frameGraph = frameGraph.get();
    _objectPickingMap = pickingMap.get();
    _renderBuffer = renderBuffer.get();

    OVITO_ASSERT(!isPickingPass() || supersamplingLevel() == 1); // Never use supersampling in picking render passes.
    OVITO_ASSERT(!isPickingPass() || orderIndependentTransparency() == false); // Never use OIT in picking render passes.

    // Obtain surface format.
    _glformat = _glcontext->format();
    OVITO_REPORT_OPENGL_ERRORS(this);

    QByteArray openGLVersionString = reinterpret_cast<const char*>(this->glGetString(GL_VERSION));
    QByteArray openGLRendererString = reinterpret_cast<const char*>(this->glGetString(GL_RENDERER));
    QByteArray openGLVendorString = reinterpret_cast<const char*>(this->glGetString(GL_VENDOR));
#ifdef Q_OS_WIN
    // OpenGL in a VirtualBox machine Windows guest reports "2.1 Chromium 1.9" as version string, which is
    // not correctly parsed by Qt. We have to work around this.
    if(openGLVersionString.startsWith("2.1 ")) {
        _glformat.setMajorVersion(2);
        _glformat.setMinorVersion(1);
    }
    if(glformat().majorVersion() < OVITO_OPENGL_MINIMUM_VERSION_MAJOR || (glformat().majorVersion() == OVITO_OPENGL_MINIMUM_VERSION_MAJOR &&
                                                                          glformat().minorVersion() < OVITO_OPENGL_MINIMUM_VERSION_MINOR)) {
        throw RendererException(tr("The OpenGL graphics driver installed on this system does not support OpenGL version %6.%7 or newer.\n\n"
                                   "OVITO requires modern graphics hardware and up-to-date graphics drivers to render 3D graphics. Your "
                                   "current system configuration is not compatible with OVITO.\n\n"
                                   "To avoid this error, please install the newest graphics driver of the hardware vendor or, if "
                                   "necessary, consider replacing your graphics card with a newer model.\n\n"
                                   "The installed OpenGL graphics driver reports the following information:\n\n"
                                   "OpenGL vendor: %1\n"
                                   "OpenGL renderer: %2\n"
                                   "OpenGL version: %3.%4 (%5)\n\n"
                                   "OVITO requires at least OpenGL version %6.%7.")
                                    .arg(QString::fromUtf8(openGLVendorString))
                                    .arg(QString::fromUtf8(openGLRendererString))
                                    .arg(glformat().majorVersion())
                                    .arg(glformat().minorVersion())
                                    .arg(QString::fromUtf8(openGLVersionString))
                                    .arg(OVITO_OPENGL_MINIMUM_VERSION_MAJOR)
                                    .arg(OVITO_OPENGL_MINIMUM_VERSION_MINOR));
    }

    if(openGLRendererString == "Intel(R) HD Graphics" || openGLRendererString == "Intel(R) HD Graphics 2000" ||
       openGLRendererString == "Intel(R) HD Graphics 3000" || openGLRendererString == "Intel(R) HD Graphics 4400") {
        throw RendererException(tr("The graphics chip of your computer is not compatible with OVITO, unfortunately.\n\n"
                                   "Intel(R) HD Graphics, an integrated graphics chip from 2010-2012, does not "
                                   "support the specific OpenGL rendering functions required by OVITO. "
                                   "There is no known workaround to make OVITO work on systems with this particular graphics unit. Please "
                                   "use OVITO on a computer with a more modern graphics processor.\n\n"
                                   "Detected graphics interface:\n\n"
                                   "OpenGL vendor: %1\n"
                                   "OpenGL renderer: %2\n"
                                   "OpenGL version: %3.%4 (%5)")
                                    .arg(QString::fromUtf8(openGLVendorString))
                                    .arg(QString::fromUtf8(openGLRendererString))
                                    .arg(glformat().majorVersion())
                                    .arg(glformat().minorVersion())
                                    .arg(QString::fromUtf8(openGLVersionString)));
    }

    if(openGLVendorString == "ATI Technologies Inc." && openGLRendererString.startsWith("AMD Radeon") && openGLRendererString.endsWith("HD 8350")) {
        throw RendererException(tr("The graphics chip of your computer is not compatible with OVITO, unfortunately.\n\n"
                                   "The AMD Radeon 8000 series does not support the specific OpenGL rendering functions required by OVITO. "
                                   "There is no known workaround to make OVITO work on systems with this particular graphics unit, which is more than a decade old. Please "
                                   "run OVITO on a computer with a more modern graphics processor.\n\n"
                                   "Detected graphics interface:\n\n"
                                   "OpenGL vendor: %1\n"
                                   "OpenGL renderer: %2\n"
                                   "OpenGL version: %3.%4 (%5)")
                                    .arg(QString::fromUtf8(openGLVendorString))
                                    .arg(QString::fromUtf8(openGLRendererString))
                                    .arg(glformat().majorVersion())
                                    .arg(glformat().minorVersion())
                                    .arg(QString::fromUtf8(openGLVersionString)));
    }
#endif

    // Open a new cache frame for the OpenGL resource management.
    _currentResourceFrame = visCache()->acquireResourceFrame();

    // Get the OpenGL version.
    _glversion = QT_VERSION_CHECK(glformat().majorVersion(), glformat().minorVersion(), 0);

    //  _glversion = QT_VERSION_CHECK(4, 1, 0);
    //  _glversion = QT_VERSION_CHECK(3, 2, 0);
    //  _glversion = QT_VERSION_CHECK(3, 1, 0);
    //  _glversion = QT_VERSION_CHECK(2, 1, 0);

    // Initialize OpenGL debug logger if requested (see OVITO_OPENGL_DEBUG_CONTEXT environment variable).
    if(glformat().testOption(QSurfaceFormat::DebugContext)) {
        QOpenGLDebugLogger* logger = glcontext()->findChild<QOpenGLDebugLogger*>();
        if(!logger) {
            logger = new QOpenGLDebugLogger(glcontext());
            QObject::connect(logger, &QOpenGLDebugLogger::messageLogged,
                             [](const QOpenGLDebugMessage& debugMessage) {
#ifndef OVITO_DEBUG
                if(debugMessage.type() == QOpenGLDebugMessage::PerformanceType)
                    return;
#endif
                qInfo() << debugMessage;
            });
        }
        logger->initialize();
        logger->startLogging();
    }

    // Get optional function pointers.
    glMultiDrawArrays = reinterpret_cast<void(QOPENGLF_APIENTRY*)(GLenum, const GLint*, const GLsizei*, GLsizei)>(
        glcontext()->getProcAddress("glMultiDrawArrays"));
    glMultiDrawArraysIndirect = reinterpret_cast<void(QOPENGLF_APIENTRY*)(GLenum, const void*, GLsizei, GLsizei)>(
        glcontext()->getProcAddress("glMultiDrawArraysIndirect"));
#ifndef Q_OS_WASM
    OVITO_ASSERT(glMultiDrawArrays);  // glMultiDrawArrays() should always be available in desktop OpenGL 2.0+.
#endif

    // Decide whether we can use the "noperspective" qualifier in fragment shaders to interpolate view ray directions.
    if(_glversion < QT_VERSION_CHECK(3, 0, 0))
        _useInterpolatedRayDirections = false;
#ifdef Q_OS_LINUX
    // Workaround for a bug in the Mesa driver on Linux: Attribute interpolation with the "noperspective" qualifier is
    // broken for partially clipped triangles.
    if(openGLRendererString.startsWith("llvmpipe"))
        _useInterpolatedRayDirections = false;
#endif

    // Set up a vertex array object (VAO). An active VAO is required during rendering according to the OpenGL 3.2 core profile.
    std::optional<QOpenGLVertexArrayObject> vertexArrayObject;
    if(glformat().majorVersion() >= 3) {
        vertexArrayObject.emplace();
        OVITO_CHECK_OPENGL(this, vertexArrayObject->create());
        OVITO_CHECK_OPENGL(this, vertexArrayObject->bind());
    }
    OVITO_REPORT_OPENGL_ERRORS(this);

    // Put the GL context into its default initial state before rendering a frame begins.
    OVITO_CHECK_OPENGL(this, this->glDisable(GL_STENCIL_TEST));
    OVITO_CHECK_OPENGL(this, this->glDisable(GL_BLEND));
    OVITO_CHECK_OPENGL(this, this->glEnable(GL_DEPTH_TEST));
    OVITO_CHECK_OPENGL(this, this->glDepthFunc(GL_LESS));
    OVITO_CHECK_OPENGL(this, this->glDepthRangef(0, 1));
    OVITO_CHECK_OPENGL(this, this->glClearDepthf(1));
    OVITO_CHECK_OPENGL(this, this->glDepthMask(GL_TRUE));
    OVITO_CHECK_OPENGL(this, this->glDisable(GL_SCISSOR_TEST));

    // Set up OpenGL render viewport.
    int viewportX = 0, viewportY = 0;
    int viewportWidth = framebufferSize().width(), viewportHeight = framebufferSize().height();
    if(frameGraph->isInteractive()) {
        // Compensate for dynamic size changes of the viewport window, which can happen before the frame graph is regenerated
        // based on the new viewport size.
        // To honor the aspect ratio of the existing frame graph, we may have to adjust the viewport rectangle.
        FloatType originalAspectRatio = frameGraph->projectionParams().aspectRatio;
        FloatType currentAspectRatio = (FloatType)viewportHeight / viewportWidth;
        viewportWidth = (int)std::lround(viewportWidth / originalAspectRatio * currentAspectRatio);
        viewportX = (framebufferSize().width() - viewportWidth) / 2;
    }
    OVITO_CHECK_OPENGL(this, this->glViewport(viewportX, viewportY, viewportWidth, viewportHeight));

    // Clear frame buffer.
    if(!isPickingPass()) {
        OVITO_CHECK_OPENGL(this, this->glClearColor(frameGraph->clearColor().r(), frameGraph->clearColor().g(),
                                                    frameGraph->clearColor().b(), frameGraph->clearColor().a()));
    }
    else {
        OVITO_CHECK_OPENGL(this, this->glClearColor(0, 0, 0, 0));
    }
    OVITO_CHECK_OPENGL(this, this->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT));
    OVITO_REPORT_OPENGL_ERRORS(this);

    // Render background graphics, typically 2D primitives only.
    _isTransparencyPass = false;
    this->glDisable(GL_DEPTH_TEST);
    renderFrameGraph(FrameGraph::UnderLayer);

    // Render fully opaque 3D geometry.
    this->glEnable(GL_DEPTH_TEST);
    bool hasTransparentGeometry = renderFrameGraph(FrameGraph::SceneLayer);

    // Perform additional steps to composite the results from multiple renderers, e.g. ANARI.
    if(_frameCompositingFunction)
        _frameCompositingFunction(*this);

    // Render translucent 3D geometry in a second pass.
    if(hasTransparentGeometry)
        renderTransparentGeometry(*renderBuffer);

    // Render highlighted geometry in a third and fourth pass.
    if(!isPickingPass() && std::any_of(frameGraph->commandGroups().cbegin(), frameGraph->commandGroups().cend(),
                   [](const FrameGraph::RenderingCommandGroup& group) { return group.layerType() == FrameGraph::HighlightLayer1; })) {

        this->glClearStencil(0);
        this->glClear(GL_STENCIL_BUFFER_BIT);
        this->glEnable(GL_STENCIL_TEST);
        this->glStencilFunc(GL_ALWAYS, 0x1, 0x1);
        this->glStencilMask(0x1);
        this->glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
#if defined(Q_OS_MACOS) && defined(Q_PROCESSOR_ARM)
        // Partial workaround for a bug in the MacOS/arm64 OpenGL implementation.
        // Fragment shaders discarding fragments (via conditional "discard") still modify the stencil buffer, which is unexpected.
        // See also: https://developer.apple.com/forums/thread/721988
        this->glStencilOp(GL_REPLACE, GL_KEEP, GL_REPLACE);
#endif
        this->glDepthFunc(GL_LEQUAL);
        this->glEnable(GL_DEPTH_TEST);

        renderFrameGraph(FrameGraph::HighlightLayer1);

        this->glStencilFunc(GL_NOTEQUAL, 0x1, 0x1);
        this->glStencilMask(0x1);
        this->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        this->glDisable(GL_DEPTH_TEST);

        renderFrameGraph(FrameGraph::HighlightLayer2);

        this->glDepthFunc(GL_LESS);
        this->glDisable(GL_STENCIL_TEST);
    }

    // Render foreground graphics, typically 2D primitives only.
    this->glDisable(GL_DEPTH_TEST);
    renderFrameGraph(FrameGraph::OverLayer);

    // Store the resource cache frame in the target frame buffer object to keep OpenGL resources alive
    // for subsequent frames.
    renderBuffer->storePreviousResourceFrame(std::move(_currentResourceFrame));

    // Read the rendered image from the OpenGL framebuffer and paint it into the output frame buffer.
    if(frameBuffer && renderBuffer->framebufferObject()) {
        // Flush the contents to the FBO before extracting the image.
        glcontext()->swapBuffers(glcontext()->surface());

        // Clear destination area in the framebuffer (only necessary if OpenGL image is not fully opaque).
        if(frameGraph->clearColor().a() != 1 && !frameBuffer->image().isNull())
            frameBuffer->clear(frameGraph->clearColor());

        // Fetch rendered image from OpenGL framebuffer.
        QImage renderedImage = renderBuffer->framebufferObject()->toImage();
        OVITO_ASSERT(renderedImage.size() == framebufferSize());
        // Rescale supersampled image to output size.
        QImage scaledImage = renderedImage.scaled(frameBuffer->viewportRect().size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);

        // Transfer OpenGL image to the output frame buffer.
        if(!frameBuffer->image().isNull()) {
            QPainter painter(&frameBuffer->image());
            painter.drawImage(frameBuffer->viewportRect(), scaledImage);
        }
        else {
            frameBuffer->image() = std::move(scaledImage);
        }
        frameBuffer->update(frameBuffer->viewportRect());
        frameBuffer->commitChanges();
    }

    // Stop debug logger.
    if(glformat().testOption(QSurfaceFormat::DebugContext)) {
        if(QOpenGLDebugLogger* logger = glcontext()->findChild<QOpenGLDebugLogger*>())
            logger->stopLogging();
    }

    // Unbind offscreen OpenGL framebuffer.
    if(renderBuffer->framebufferObject().has_value() && !renderBuffer->framebufferObject()->release())
        throw RendererException(tr("Failed to release OpenGL framebuffer object after offscreen rendering."));

    _glcontext = nullptr;
    _objectPickingMap = nullptr;
    _renderBuffer = nullptr;

    return SCFuture<void>::createImmediateEmpty();
}

/******************************************************************************
 * Renders all semi-transparent geometry in a second rendering pass.
 ******************************************************************************/
void OpenGLRenderingJob::renderTransparentGeometry(OpenGLRenderBuffer& frameBuffer)
{
    // Semi-transparent geometry should never get rendered in a picking render pass.
    OVITO_ASSERT(!isPickingPass());

    // Implementation of the "Weighted Blended Order-Independent Transparency" method.
    if(orderIndependentTransparency()) frameBuffer.beginOITRendering();

    _isTransparencyPass = true;
    renderFrameGraph(FrameGraph::SceneLayer);
    _isTransparencyPass = false;

    // Second phase of the "Weighted Blended Order-Independent Transparency" method.
    if(orderIndependentTransparency()) frameBuffer.endOITRendering();
}

/******************************************************************************
 * Sets up the model-view transformation matrix for the given rendering command.
 ******************************************************************************/
void OpenGLRenderingJob::setupModelViewTransformation(const FrameGraph::RenderingCommand& command)
{
    if(command.modelWorldTM() != AffineTransformation::Zero()) {
        _preprojectedCoordinates = false;
        _modelViewTM = frameGraph()->projectionParams().viewMatrix * command.modelWorldTM();
    }
    else {
        _preprojectedCoordinates = true;
        _modelViewTM.setZero();
    }
}

/******************************************************************************
 * Executes the rendering commands stored in the given frame graph.
 ******************************************************************************/
bool OpenGLRenderingJob::renderFrameGraph(FrameGraph::RenderLayerType layerType)
{
    bool hasTransparentGeometry = false;

    for(const FrameGraph::RenderingCommandGroup& commandGroup : frameGraph()->commandGroups()) {
        // Skip command groups that are not part of the current render layer.
        if(commandGroup.layerType() != layerType)
            continue;

        for(const FrameGraph::RenderingCommand& command : commandGroup.commands()) {
            // Skip commands that are not relevant for the current rendering pass.
            if(isPickingPass()) {
                if(command.skipInPickingPass())
                    continue;
            }
            else {
                if(command.skipInVisualPass())
                    continue;
            }
            // Query external filter whether to skip this command.
            if(renderingCommandFilter(commandGroup, command))
                continue;

            // Set up the model-view transformation matrix.
            setupModelViewTransformation(command);

            if(const ParticlePrimitive* primitive = dynamic_cast<const ParticlePrimitive*>(command.primitive())) {
                hasTransparentGeometry |= renderParticles(*primitive, command);
            }
            else if(const CylinderPrimitive* primitive = dynamic_cast<const CylinderPrimitive*>(command.primitive())) {
                hasTransparentGeometry |= renderCylinders(*primitive, command);
            }
            else if(const MeshPrimitive* primitive = dynamic_cast<const MeshPrimitive*>(command.primitive())) {
                hasTransparentGeometry |= renderMesh(*primitive, command);
            }
            else if(!isTransparencyPass()) {
                if(const LinePrimitive* primitive = dynamic_cast<const LinePrimitive*>(command.primitive())) {
                    renderLinesImplementation(*primitive, command);
                }
                else if(const ImagePrimitive* primitive = dynamic_cast<const ImagePrimitive*>(command.primitive())) {
                    renderImageImplementation(*primitive);
                }
                else if(const MarkerPrimitive* primitive = dynamic_cast<const MarkerPrimitive*>(command.primitive())) {
                    renderMarkersImplementation(*primitive, command);
                }
            }
            OVITO_REPORT_OPENGL_ERRORS(this);
        }
    }
    return hasTransparentGeometry;
}

/******************************************************************************
 * Renders a particles primitive.
 ******************************************************************************/
bool OpenGLRenderingJob::renderParticles(const ParticlePrimitive& primitive, const FrameGraph::RenderingCommand& command)
{
    // Render particles immediately if they are all fully opaque. Otherwise defer rendering to a later time.
    if(isPickingPass() || isTransparencyPass() != (!primitive.transparencies())) {
        renderParticlesImplementation(primitive, command);
        return false;
    }
    else {
        if(orderIndependentTransparency() && primitive.transparencies()) {
            // The order-independent transparency method does not support fully opaque geometry (transparency=0) very well.
            // Any such geometry still appears translucent and does not fully occlude the objects behind it. To mitigate the problem,
            // we render the fully opaque geometry already during the first rendering pass to fill the z-buffer.
            const ConstDataBufferPtr& opaqueIndices = currentResourceFrame().lookup<ConstDataBufferPtr>(
                RendererResourceKey<struct OpaqueParticlesCacheKey, ConstDataBufferPtr, ConstDataBufferPtr>(primitive.transparencies(), primitive.indices()),
                [&](ConstDataBufferPtr& opaqueIndices) {
                    // Are there any particles having a non-positive transparency value?
                    std::vector<int32_t> fullyOpaqueIndices;
                    if(!primitive.indices()) {
                        int index = 0;
                        for(FloatType t : BufferReadAccess<GraphicsFloatType>(primitive.transparencies())) {
                            if(t <= 0) fullyOpaqueIndices.push_back(index);
                            index++;
                        }
                    }
                    else {
                        BufferReadAccess<GraphicsFloatType> transparencies(primitive.transparencies());
                        for(auto index : BufferReadAccess<int32_t>(primitive.indices())) {
                            if(transparencies[index] <= 0) fullyOpaqueIndices.push_back(index);
                        }
                    }
                    if(!fullyOpaqueIndices.empty()) {
                        opaqueIndices = BufferFactory<int32_t>(fullyOpaqueIndices.begin(), fullyOpaqueIndices.end()).take();
                    }
                });

            if(opaqueIndices) {
                ParticlePrimitive opaqueParticles = primitive;
                opaqueParticles.setTransparencies({});
                opaqueParticles.setIndices(opaqueIndices);
                renderParticlesImplementation(opaqueParticles, command);
            }
        }
        return true;
    }
}

/******************************************************************************
 * Renders a cylinders primitive.
 ******************************************************************************/
bool OpenGLRenderingJob::renderCylinders(const CylinderPrimitive& primitive, const FrameGraph::RenderingCommand& command)
{
    // Render primitives immediately if they are all fully opaque. Otherwise defer rendering to a later time.
    if(isPickingPass() || isTransparencyPass() != (!primitive.transparencies())) {
        renderCylindersImplementation(primitive, command);
        return false;
    }
    return true;
}

/******************************************************************************
 * Renders a triangle mesh primitive.
 ******************************************************************************/
bool OpenGLRenderingJob::renderMesh(const MeshPrimitive& primitive, const FrameGraph::RenderingCommand& command)
{
    // Render mesh immediately if it is fully opaque. Otherwise defer rendering to a later time.
    if(isPickingPass() || isTransparencyPass() != primitive.isFullyOpaque()) {
        renderMeshImplementation(primitive, command);
        return false;
    }
    return true;
}

/******************************************************************************
 * Loads an OpenGL shader program.
 ******************************************************************************/
QOpenGLShaderProgram* OpenGLRenderingJob::loadShaderProgram(const QString& id, const QString& vertexShaderFile,
                                                            const QString& fragmentShaderFile, const QString& geometryShaderFile)
{
    QOpenGLContextGroup* contextGroup = QOpenGLContextGroup::currentContextGroup();
    OVITO_ASSERT(contextGroup);

    OVITO_ASSERT(QOpenGLShaderProgram::hasOpenGLShaderPrograms());
    OVITO_ASSERT(QOpenGLShader::hasOpenGLShaders(QOpenGLShader::Vertex));
    OVITO_ASSERT(QOpenGLShader::hasOpenGLShaders(QOpenGLShader::Fragment));

    // Are we doing the transparency pass for "Weighted Blended Order-Independent Transparency"?
    bool isWBOITPass = (isTransparencyPass() && orderIndependentTransparency());

    // Compile a modified version of each shader for the transparency pass.
    // This is accomplished by giving the shader a unique identifier.
    QString mangledId = id;
    if(isWBOITPass) mangledId += QStringLiteral(".wboi_transparency");

    // Guard concurrent access to the shared shader cache.
    static std::mutex sharedShaderMutex;
    std::lock_guard<std::mutex> lock(sharedShaderMutex);

    // Each OpenGL shader is only created once per OpenGL context group.
    std::unique_ptr<QOpenGLShaderProgram> program(contextGroup->findChild<QOpenGLShaderProgram*>(mangledId));
    if(program)
        return program.release();

    // The program's source code hasn't been compiled so far. Do it now and cache the shader program.
    program = std::make_unique<QOpenGLShaderProgram>();
    program->setObjectName(mangledId);

    // Load and compile vertex shader source.
    loadShader(program.get(), QOpenGLShader::Vertex, vertexShaderFile, isWBOITPass);

    // Load and compile fragment shader source.
    loadShader(program.get(), QOpenGLShader::Fragment, fragmentShaderFile, isWBOITPass);

    // Load and compile geometry shader source.
    if(!geometryShaderFile.isEmpty()) {
        loadShader(program.get(), QOpenGLShader::Geometry, geometryShaderFile, isWBOITPass);
    }

    // Compile the shader program.
    if(!program->link()) {
        RendererException ex(QString("The OpenGL shader program %1 failed to link.").arg(mangledId));
        ex.appendDetailMessage(program->log());
        throw ex;
    }

    OVITO_REPORT_OPENGL_ERRORS(this);

    // Make the shader program a child object of the GL context group.
    if(program->thread() == contextGroup->thread()) {
        program->setParent(contextGroup);
    }
    else {
        program->moveToThread(contextGroup->thread());
        // Make the program object a child of the context group object in the main thread to follow the thread-affinity rules of Qt.
        Application::instance()->taskManager().submitWork([program=program.get(), contextGroup=QPointer<QOpenGLContextGroup>(contextGroup)]() noexcept {
            if(!contextGroup.isNull())
                program->setParent(contextGroup);
            else
                program->deleteLater();
        });
    }

    return program.release();
}

/******************************************************************************
 * Loads and compiles a GLSL shader and adds it to the given program object.
 ******************************************************************************/
void OpenGLRenderingJob::loadShader(QOpenGLShaderProgram* program, QOpenGLShader::ShaderType shaderType, const QString& filename,
                                    bool isWBOITPass)
{
    QByteArray shaderSource;
    bool isGLES = QOpenGLContext::currentContext()->isOpenGLES();
    int glslVersion = 0;

    // Insert GLSL version string at the top.
    // Pick GLSL language version based on current OpenGL version.
    if(!isGLES) {
        // Inject GLSL version directive into shader source.
        if(_glversion >= QT_VERSION_CHECK(3, 3, 0)) {
            shaderSource.append("#version 330\n");
            glslVersion = QT_VERSION_CHECK(3, 3, 0);
        }
        else if(shaderType == QOpenGLShader::Geometry || _glversion >= QT_VERSION_CHECK(3, 2, 0)) {
            shaderSource.append("#version 150\n");
            glslVersion = QT_VERSION_CHECK(1, 5, 0);
        }
        else if(_glversion >= QT_VERSION_CHECK(3, 1, 0)) {
            shaderSource.append("#version 140\n");
            glslVersion = QT_VERSION_CHECK(1, 4, 0);
        }
        else if(_glversion >= QT_VERSION_CHECK(3, 0, 0)) {
            shaderSource.append("#version 130\n");
            glslVersion = QT_VERSION_CHECK(1, 3, 0);
        }
        else {
            shaderSource.append("#version 120\n");
            glslVersion = QT_VERSION_CHECK(1, 2, 0);
        }
    }
    else {
        // Using OpenGL ES context.
        // Inject GLSL version directive into shader source.
        if(glformat().majorVersion() >= 3) {
            shaderSource.append("#version 300 es\n");
            glslVersion = QT_VERSION_CHECK(3, 0, 0);
        }
        else {
            glslVersion = QT_VERSION_CHECK(1, 2, 0);
            shaderSource.append("precision highp float;\n");

            if(shaderType == QOpenGLShader::Fragment) {
                // OpenGL ES 2.0 has no built-in support for gl_FragDepth.
                // Need to request EXT_frag_depth extension in such a case.
                shaderSource.append("#extension GL_EXT_frag_depth : enable\n");
                // Computation of local normal vectors in fragment shaders requires GLSL
                // derivative functions dFdx, dFdy.
                shaderSource.append("#extension GL_OES_standard_derivatives : enable\n");
            }

            // Provide replacements of some missing GLSL functions in OpenGL ES Shading Language.
            shaderSource.append("mat3 transpose(in mat3 tm) {\n");
            shaderSource.append("    vec3 i0 = tm[0];\n");
            shaderSource.append("    vec3 i1 = tm[1];\n");
            shaderSource.append("    vec3 i2 = tm[2];\n");
            shaderSource.append("    mat3 out_tm = mat3(\n");
            shaderSource.append("         vec3(i0.x, i1.x, i2.x),\n");
            shaderSource.append("         vec3(i0.y, i1.y, i2.y),\n");
            shaderSource.append("         vec3(i0.z, i1.z, i2.z));\n");
            shaderSource.append("    return out_tm;\n");
            shaderSource.append("}\n");
        }
    }

    if(_glversion < QT_VERSION_CHECK(3, 0, 0)) {
        // This is needed to emulate the special shader variables 'gl_VertexID' and 'gl_InstanceID' in GLSL 1.20:
        if(shaderType == QOpenGLShader::Vertex) {
            // Note: Data type 'float' is used for the vertex attribute, because some OpenGL implementation have poor support for integer
            // vertex attributes.
            shaderSource.append("attribute float vertexID;\n");
            shaderSource.append("uniform int vertices_per_instance;\n");
        }
    }
    else if(!useInstancedArrays()) {
        // This is needed to compute the special shader variable 'gl_VertexID' when instanced arrays are not supported:
        if(shaderType == QOpenGLShader::Vertex) {
            shaderSource.append("uniform int vertices_per_instance;\n");
        }
    }

    if(!isWBOITPass) {
        // Declare the fragment color output variable referenced by the <fragColor> placeholder.
        if(_glversion >= QT_VERSION_CHECK(3, 0, 0)) {
            if(shaderType == QOpenGLShader::Fragment) {
                shaderSource.append("out vec4 fragColor;\n");
            }
        }
    }
    else {
        // Declare the fragment output variables referenced by the <fragAccumulation> and <fragRevealage> placeholders.
        if(shaderType == QOpenGLShader::Fragment) {
            if(glslVersion >= QT_VERSION_CHECK(3, 0, 0)) {
                if(glslVersion >= QT_VERSION_CHECK(3, 3, 0)) {
                    shaderSource.append("layout(location = 0) out vec4 fragAccumulation;\n");
                    shaderSource.append("layout(location = 1) out float fragRevealage;\n");
                }
                else {
                    shaderSource.append("out vec4 fragAccumulation;\n");
                    shaderSource.append("out float fragRevealage;\n");
                    if(QOpenGLFunctions_3_0* glfunc30 = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_3_0>(glcontext())) {
                        OVITO_CHECK_OPENGL(this, glfunc30->glBindFragDataLocation(program->programId(), 0, "fragAccumulation"));
                        OVITO_CHECK_OPENGL(this, glfunc30->glBindFragDataLocation(program->programId(), 1, "fragRevealage"));
                    }
                    else
                        qWarning() << "WARNING: Could not resolve OpenGL 3.0 API functions.";
                }
            }
        }
    }

    // Helper function that appends a source code line to the buffer after preprocessing it.
    auto preprocessShaderLine = [&](QByteArray& line) {
        if(_glversion < QT_VERSION_CHECK(3, 0, 0)) {
            // Automatically back-port shader source code to make it compatible with OpenGL 2.1 (GLSL 1.20):
            if(shaderType == QOpenGLShader::Vertex) {
                if(line.startsWith("in "))
                    line = QByteArrayLiteral("attribute") + line.mid(2);
                else if(line.startsWith("out "))
                    line = QByteArrayLiteral("varying") + line.mid(3);
                else if(line.startsWith("flat out "))
                    line = QByteArrayLiteral("varying") + line.mid(8);
                else {
                    if(!isGLES) {
                        line.replace("float(objectID & 0xFF)", "floor(mod(objectID, 256.0))");
                        line.replace("float((objectID >> 8) & 0xFF)", "floor(mod(objectID / 256.0, 256.0))");
                        line.replace("float((objectID >> 16) & 0xFF)", "floor(mod(objectID / 65536.0, 256.0))");
                        line.replace("float((objectID >> 24) & 0xFF)", "floor(mod(objectID / 16777216.0, 256.0))");
                    }
                    else {
                        line.replace("float(objectID & 0xFF)", "floor(mod(float(objectID), 256.0))");
                        line.replace("float((objectID >> 8) & 0xFF)", "floor(mod(float(objectID) / 256.0, 256.0))");
                        line.replace("float((objectID >> 16) & 0xFF)", "floor(mod(float(objectID) / 65536.0, 256.0))");
                        line.replace("float((objectID >> 24) & 0xFF)", "floor(mod(float(objectID) / 16777216.0, 256.0))");
                    }
                }
            }
            else if(shaderType == QOpenGLShader::Fragment) {
                if(line.startsWith("in "))
                    line = QByteArrayLiteral("varying") + line.mid(2);
                else if(line.startsWith("flat in "))
                    line = QByteArrayLiteral("varying") + line.mid(7);
                else if(line.startsWith("out "))
                    return;
            }
        }

        if(!isWBOITPass) {
            // Writing to the fragment color output variable.
            if(_glversion < QT_VERSION_CHECK(3, 0, 0))
                line.replace("<fragColor>", "gl_FragColor");
            else
                line.replace("<fragColor>", "fragColor");
        }
        else {
            if(glslVersion < QT_VERSION_CHECK(3, 0, 0))
                line.replace("<fragAccumulation>", "gl_FragData[0]");
            else
                line.replace("<fragAccumulation>", "fragAccumulation");

            if(glslVersion < QT_VERSION_CHECK(3, 0, 0))
                line.replace("<fragRevealage>", "gl_FragData[1].r");
            else
                line.replace("<fragRevealage>", "fragRevealage");
        }

        // Writing to the fragment depth output variable.
        if(_glversion >= QT_VERSION_CHECK(3, 0, 0) || isGLES == false)
            line.replace("<fragDepth>", "gl_FragDepth");
        else if(line.contains("<fragDepth>")) {  // For GLES2:
            line.replace("<fragDepth>", "gl_FragDepthEXT");
            line.prepend(QByteArrayLiteral("#if defined(GL_EXT_frag_depth)\n"));
            line.append(QByteArrayLiteral("#endif\n"));
        }

        // Old GLSL versions do not provide an inverse() function for mat3 matrices.
        // Replace calls to the inverse() function with a custom implementation.
        if(_glversion < QT_VERSION_CHECK(3, 3, 0))
            line.replace("<inverse_mat3>", "inverse_mat3");  //  Emulate inverse(mat3) with own function.
        else
            line.replace("<inverse_mat3>", "inverse");  // inverse(mat3) is natively supported.

        // The per-instance vertex ID.
        if(_glversion < QT_VERSION_CHECK(3, 0, 0))
            line.replace("<VertexID>", "int(mod(vertexID + 0.5, float(vertices_per_instance)))");  // gl_VertexID is not available, requires
                                                                                                   // a VBO with explicit vertex IDs
        else if(!useInstancedArrays())
            line.replace("<VertexID>", "(gl_VertexID % vertices_per_instance)");  // gl_VertexID is available but no instanced arrays.
        else
            line.replace("<VertexID>", "gl_VertexID");  // gl_VertexID is fully supported.

        // The instance ID.
        if(_glversion < QT_VERSION_CHECK(3, 0, 0))
            line.replace("<InstanceID>", "(int(vertexID) / vertices_per_instance)");  // Compute the instance ID from the running vertex
                                                                                      // index, which is read from a VBO array.
        else if(!useInstancedArrays())
            line.replace("<InstanceID>",
                         "(gl_VertexID / vertices_per_instance)");  // Compute the instance ID from the running vertex index.
        else
            line.replace("<InstanceID>", "gl_InstanceID");  // gl_InstanceID is fully supported.

        // 1-D texture sampler.
        if(_glversion < QT_VERSION_CHECK(3, 0, 0))
            line.replace("<texture1D>", "texture1D");
        else
            line.replace("<texture1D>", "texture");

        // 2-D texture sampler.
        if(_glversion < QT_VERSION_CHECK(3, 0, 0))
            line.replace("<texture2D>", "texture2D");
        else
            line.replace("<texture2D>", "texture");

        // View ray calculation in vertex and geometry shaders.
        if(line.contains("<calculate_view_ray_through_vertex>")) {
            if(_useInterpolatedRayDirections)
                line.replace("<calculate_view_ray_through_vertex>", "calculate_view_ray_through_vertex()");
            else
                return;  // Skip view ray calculation in vertex/geometry shader and let the fragment shader do the full calculation for
                         // each fragment.
        }

        // View ray calculation in fragment shaders.
        if(line.contains("<calculate_view_ray_through_fragment>")) {
            if(_useInterpolatedRayDirections) {
                // Calculate view ray based on interpolated values coming from the vertex shader.
                line.replace("<calculate_view_ray_through_fragment>", "vec3 ray_dir_norm = normalize(ray_dir);");
            }
            else {
                // Perform full view ray computation in the fragment shader's main function.
                line.replace("<calculate_view_ray_through_fragment>",
                             "vec2 viewport_position = ((gl_FragCoord.xy - viewport_origin) * inverse_viewport_size) - 1.0;\n"
                             "vec4 _near = inverse_projection_matrix * vec4(viewport_position, -1.0, 1.0);\n"
                             "vec4 _far = _near + inverse_projection_matrix[2];\n"
                             "vec3 ray_origin = _near.xyz / _near.w;\n"
                             "vec3 ray_dir_norm = normalize(_far.xyz / _far.w - ray_origin);\n");
            }
        }

        // Flat surface normal calculation in vertex and geometry shaders.
        if(line.contains("<flat_normal.output>")) {
            if(_glversion >= QT_VERSION_CHECK(3, 0, 0)) {
                line.replace("<flat_normal.output>", "flat_normal_fs");  // Note: "flat_normal_fs" is defined in "flat_normal.vert".
            }
            else {
                // Pass view-space coordinates of vertex to fragment shader as texture coordinates.
                if(!isGLES)
                    line = "gl_TexCoord[1] = inverse_projection_matrix * gl_Position;\n";
                else
                    line = "tex_coords = (inverse_projection_matrix * gl_Position).xyz;\n";
            }
        }

        // Flat surface normal calculation in fragment shaders.
        if(line.contains("<flat_normal.input>")) {
            if(_glversion >= QT_VERSION_CHECK(3, 0, 0)) {
                line.replace("<flat_normal.input>", "flat_normal_fs");  // Note: "flat_normal_fs" is defined in "flat_normal.frag".
            }
            else {
                // Calculate surface normal from cross product of UV tangents.
                line.replace("<flat_normal.input>", !isGLES ? "normalize(cross(dFdx(gl_TexCoord[1].xyz), dFdy(gl_TexCoord[1].xyz))"
                                                            : "normalize(cross(dFdx(tex_coords), dFdy(tex_coords))");
            }
        }

        shaderSource.append(line);
    };

    // Load actual shader source code.
    QFile shaderSourceFile(filename);
    if(!shaderSourceFile.open(QFile::ReadOnly)) throw RendererException(QString("Unable to open shader source file %1.").arg(filename));

    // Parse each line of the shader file and process #include directives.
    while(!shaderSourceFile.atEnd()) {
        QByteArray line = shaderSourceFile.readLine();
        if(line.startsWith("#include")) {
            QString includeFilePath;

            // Special include statement which require preprocessing.
            if(line.contains("<shading.frag>")) {
                if(!isWBOITPass)
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/shading.frag");
                else
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/shading_transparency.frag");
            }
            else if(line.contains("<view_ray.vert>")) {
                if(!_useInterpolatedRayDirections)
                    continue;  // Skip this include file, because view ray calculation is performed by the fragment shaders in old GLSL
                               // versions.
                includeFilePath = QStringLiteral(":/openglrenderer/glsl/view_ray.vert");
            }
            else if(line.contains("<view_ray.frag>")) {
                if(!_useInterpolatedRayDirections)
                    continue;  // Skip this include file, because view ray calculation is performed by the fragment shaders in old GLSL
                               // versions.
                includeFilePath = QStringLiteral(":/openglrenderer/glsl/view_ray.frag");
            }
            else if(line.contains("<flat_normal.vert>")) {
                if(_glversion >= QT_VERSION_CHECK(3, 0, 0))
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/flat_normal.vert");
                else if(isGLES)
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/flat_normal.GLES.vert");
                else
                    continue;
            }
            else if(line.contains("<flat_normal.frag>")) {
                if(_glversion >= QT_VERSION_CHECK(3, 0, 0))
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/flat_normal.frag");
                else if(isGLES)
                    includeFilePath = QStringLiteral(":/openglrenderer/glsl/flat_normal.GLES.frag");
                else
                    continue;
            }
            else {
                // Resolve relative file paths.
                QFileInfo includeFile(QFileInfo(shaderSourceFile).dir(), QString::fromUtf8(line.mid(8).replace('\"', "").trimmed()));
                includeFilePath = includeFile.filePath();
            }

            // Load the secondary shader file and insert it into the source of the primary shader.
            QFile secondarySourceFile(includeFilePath);
            if(!secondarySourceFile.open(QFile::ReadOnly))
                throw RendererException(QString("Unable to open shader source file %1 referenced by include directive in shader file %2.")
                                            .arg(includeFilePath)
                                            .arg(filename));
            while(!secondarySourceFile.atEnd()) {
                line = secondarySourceFile.readLine();
                preprocessShaderLine(line);
            }
            shaderSource.append('\n');
        }
        else {
            preprocessShaderLine(line);
        }
    }

    // Load and compile vertex shader source.
    if(!program->addShaderFromSourceCode(shaderType, shaderSource)) {
        RendererException ex(QString("The shader source file %1 failed to compile.").arg(filename));
        ex.appendDetailMessage(program->log());
        ex.appendDetailMessage(QStringLiteral("Problematic shader source:"));
        ex.appendDetailMessage(shaderSource);
        throw ex;
    }

    OVITO_REPORT_OPENGL_ERRORS(this);
}

/******************************************************************************
 * Translates an OpenGL error code to a human-readable message string.
 ******************************************************************************/
static const char* openglErrorString(GLenum errorCode)
{
    switch(errorCode) {
        case GL_NO_ERROR: return "GL_NO_ERROR - No error has been recorded.";
        case GL_INVALID_ENUM: return "GL_INVALID_ENUM - An unacceptable value is specified for an enumerated argument.";
        case GL_INVALID_VALUE: return "GL_INVALID_VALUE - A numeric argument is out of range.";
        case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION - The specified operation is not allowed in the current state.";
        case 0x0503 /*GL_STACK_OVERFLOW*/: return "GL_STACK_OVERFLOW - This command would cause a stack overflow.";
        case 0x0504 /*GL_STACK_UNDERFLOW*/: return "GL_STACK_UNDERFLOW - This command would cause a stack underflow.";
        case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY - There is not enough memory left to execute the command.";
        case 0x8031 /*GL_TABLE_TOO_LARGE*/:
            return "GL_TABLE_TOO_LARGE - The specified table exceeds the implementation's maximum supported table size.";
        case 0x0506 /*GL_INVALID_FRAMEBUFFER_OPERATION*/:
            return "GL_INVALID_FRAMEBUFFER_OPERATION - The read and draw framebuffers are not framebuffer complete.";
        default: return "Unknown OpenGL error code.";
    }
}

/******************************************************************************
 * Reports OpenGL error status codes.
 ******************************************************************************/
void OpenGLRenderingJob::checkOpenGLErrorStatus(const char* command, const char* sourceFile, int sourceLine)
{
    GLenum error;
    while((error = this->glGetError()) != GL_NO_ERROR) {
        qDebug() << "WARNING: OpenGL call" << command
                 << "failed "
                    "in line"
                 << sourceLine << "of file" << sourceFile << "with error" << openglErrorString(error);
    }
}

/******************************************************************************
 * Create an OpenGL texture object for a QImage.
 ******************************************************************************/
const OpenGLTexture& OpenGLRenderingJob::uploadImage(const QImage& image)
{
    OVITO_ASSERT(!image.isNull());

    // Check if this image has already been uploaded to the GPU.
    return currentResourceFrame().lookup<OpenGLTexture>(
        RendererResourceKey<struct ImageCache, quint64, const QOpenGLContextGroup*>{image.cacheKey(), QOpenGLContextGroup::currentContextGroup()},
        [&](OpenGLTexture& texture) {
            texture.create(image);
            texture.setMinMagFilters(QOpenGLTexture::Nearest, QOpenGLTexture::Nearest);
        });
}

/******************************************************************************
 * Creates a 1-D OpenGL texture object for a ColorCodingGradient.
 ******************************************************************************/
const OpenGLTexture& OpenGLRenderingJob::uploadColorMap(const PseudoColorMapping& pseudoColorMapping)
{
    // Bin count of the discrete color map.
    // Use discrete color mapping if the bin count is > 0
    const int numDiscreteColors = pseudoColorMapping.discreteColorMapBinCount();

    // Check if this color map has already been uploaded to the GPU.
    return currentResourceFrame().lookup<OpenGLTexture>(
        RendererResourceKey<struct ColorMapCache, OORef<const ColorCodingGradient>, const QOpenGLContextGroup*, int>{
            pseudoColorMapping.gradient(), QOpenGLContextGroup::currentContextGroup(), pseudoColorMapping.discreteColorMapBinCount()},
        [&](OpenGLTexture& texture) {
            // Sample the color gradient to produce a row of RGB pixel data.
            int resolution;
            std::vector<uint8_t> pixelData;

            if(pseudoColorMapping.gradient()) {
                // Set the resolution to the number of discrete colors if specified, otherwise use 256.
                resolution = (numDiscreteColors <= 0) ? 256 : std::min(256, numDiscreteColors);
                pixelData.resize((size_t)resolution * 3);
                for(int x = 0; x < resolution; x++) {
                    float t = (float)x / float(resolution - 1);
                    // Use discrete color mapping if the bin count is > 0
                    t = (numDiscreteColors <= 0) ? t : DiscreteColorMap::mapValue(t, numDiscreteColors);
                    auto c = pseudoColorMapping.gradient()->valueToColor(t);
                    pixelData[x * 3 + 0] = (uint8_t)(255 * c.r());
                    pixelData[x * 3 + 1] = (uint8_t)(255 * c.g());
                    pixelData[x * 3 + 2] = (uint8_t)(255 * c.b());
                }
            }
            else {
                resolution = 1;
                pixelData.resize(3, 255);
            }

            // Create the 1-d texture object.
            texture.create(QOpenGLTexture::Target2D);
            texture.setWrapMode(QOpenGLTexture::ClampToEdge);
            // Use nearest filter for discrete color map to avoid color interpolation
            // Use linear in all other cases to avoid artifacts.
            const QOpenGLTexture::Filter minMagFilter = (numDiscreteColors <= 0) ? QOpenGLTexture::Linear : QOpenGLTexture::Nearest;
            texture.setMinMagFilters(minMagFilter, minMagFilter);  // Note: Other modes may cause artifacts, e.g., at cylinder ends.
            texture.setData(
                QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::RGB, QOpenGLTexture::UInt8,
                resolution, 1,
                pixelData.data(),
                false);
        });
}

}  // namespace Ovito
