2
votes

I've been struggling lately when i had to change some code that i wrote a while ago to do image processing in Qt and OpenGl to support multithreading.

the problem is that I want to use it to apply batches of filters on a set of images, i'm using openMP to do the multi-threading like this:

void batchProcess(QVector<QImage> &images)
{
    #pragma omp parallel
    {
    #pragma omp for schedule(dynamic) nowait
        for (int i = 1; i < images.count(); i++)
        {
            images[i] = ImageProcessing::processImage(images[i], _vertexShader, _fragmentShader, _textureVar, _vertexPosVar, _textureCoordVar);
        }
    }
}

and the ImageProcessing::processImage() function looks like this:

QImage ImageProcessing::processImage(const QImage &source, const QString &vertexShader, const QString &fragmentShader, const QString &textureVar, const QString &vertexPosVar, const QString &textureCoordVar)
{
    QOpenGLContext context;
    if(!context.create())
        return source;

    QOffscreenSurface surface;
    surface.setFormat(context.format());
    surface.create();
    if(!surface.isValid())
        return source;

    if(!context.makeCurrent(&surface))
        return source;

    QOpenGLFramebufferObject fbo(source.size());
    context.functions()->glViewport(0, 0, source.width(), source.height());

    QOpenGLShaderProgram program(&context);
    if (!program.addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShader))
        return source;

    if (!program.addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShader))
        return source;

    if (!program.link())
        return source;

    if (!program.bind())
        return source;

    QOpenGLTexture texture(QOpenGLTexture::Target2D);
    texture.setData(source);

    texture.bind();
    if(!texture.isBound())
        return source;

    VertexData vertices[] =
    {
        {{ -1.0f, +1.0f }, { 0.0f, 1.0f }}, // top-left
        {{ +1.0f, +1.0f }, { 1.0f, 1.0f }}, // top-right
        {{ -1.0f, -1.0f }, { 0.0f, 0.0f }}, // bottom-left
        {{ +1.0f, -1.0f }, { 1.0f, 0.0f }}  // bottom-right
    };

    GLuint indices[] =
    {
        0, 1, 2, 3
    };

    QOpenGLBuffer vertexBuf(QOpenGLBuffer::VertexBuffer);
    QOpenGLBuffer indexBuf(QOpenGLBuffer::IndexBuffer);

    if(!vertexBuf.create())
        return source;

    if(!indexBuf.create())
        return source;

    if(!vertexBuf.bind())
        return source;

    vertexBuf.allocate(vertices, 4 * sizeof(VertexData));

    if(!indexBuf.bind())
        return source;
        
    indexBuf.allocate(indices, 4 * sizeof(GLuint));

    int offset = 0;
    program.enableAttributeArray(vertexPosVar.toLatin1().data());
    program.setAttributeBuffer(vertexPosVar.toLatin1().data(), GL_FLOAT, offset, 2, sizeof(VertexData));
    offset += sizeof(QVector2D);
    program.enableAttributeArray(textureCoordVar.toLatin1().data());
    program.setAttributeBuffer(textureCoordVar.toLatin1().data(), GL_FLOAT, offset, 2, sizeof(VertexData));
    program.setUniformValue(textureVar.toLatin1().data(), 0);

    context.functions()->glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_INT, Q_NULLPTR);

    return fbo.toImage(false);
}

it works fine when OpenMP is disabled, but when i enable it i get the following error:

Attempting to create QWindow-based QOffscreenSurface outside the gui thread. Expect failures.

according to the documentation, this is an expected behavior, because the QOffscreenSurface is just a hidden QWindow on some platforms, which means that it can only be created or destroyed from the main thread.

so in order to create the QOffscreenSurface in the Main thread i followed this answer and implemented the following class

class OpenGlFilterPrivate : public QObject
{
    Q_OBJECT
public:
    enum CustomEventTypes
    {
        CreateSurface = QEvent::User,
        CreateContext = QEvent::User + 1,
        CreateProgram = QEvent::User + 2
    };

    void moveToMainThread()
    {
        moveToThread(QApplication::instance()->thread());
    }

    virtual bool event(QEvent *event )
    {
        switch (event->type())
        {
        case CreateSurface: // m_surface (create an offscreen surface from the main thread)
            m_surface = QSharedPointer<QOffscreenSurface>::create();

            m_surface->setFormat(m_context->format());
            m_surface->create();
            break;

        case CreateContext: // m_context (create an openGl context from the main thread)
            m_context = QSharedPointer<QOpenGLContext>::create();
            break;

        case CreateProgram: // m_shaderProgram (create an openGl shader program from the main thread)
            m_shaderProgram = QSharedPointer<QOpenGLShaderProgram>::create(&*m_context);
        }

        return false;
    }

    QSharedPointer<QOpenGLContext> m_context;
    QSharedPointer<QOffscreenSurface> m_surface;
    QSharedPointer<QOpenGLShaderProgram> m_shaderProgram;
};

and now instead of directly creating a QOffscreenSurface in my processImage function i do the following:

OpenGlFilterPrivate openGlFilterPrivate;
openGlFilterPrivate.moveToMainThread();
QCoreApplication::postEvent(&openGlFilterPrivate, new QEvent(QEvent::Type(OpenGlFilterPrivate::CreateSurface)));
QSharedPointer<QOffscreenSurface> surface = openGlFilterPrivate.m_surface;

it works, but now i keep getting the following message:

QObject::~QObject: Timers cannot be stopped from another thread

and then my application crashes immediately.

i know that any QObject contains a QTimer internally and that this is what's causing the problem, but i can't think of any other way to get around this.

Can anyone help with this? all i need to do is to apply some image processing filters using openGl or any other hardware accelerated method and at the same time have the ability to do it in a thread-safe way.

  • i think it can be done using the same method used by QPainter, it's thread safe and as far as i know it's hardware accelerated using OpenGl. but i couldn't find any resources on how to do such thing.
1

1 Answers

2
votes

Oh my, OpenMP and OpenGL probably don't mix very well. The way OpenMP creates multiple threads is not pinned down in terms that would be accessible to OpenGL implementations. Most likely it's using regular POSIX threads or Windows native threads. But it could also be anything else, that creates additional tasks inside the same address space.

I think it'd be far more robust and easier to tackle to create a pool of N threads, each with its own sharing OpenGL context and then use work stealing to schedule the tasks; keep the OpenGL current on the threads between worklets.

Conveniently enough with modern OpenGL one doesn't even need a window or similar drawable to make a context current. glXMakeContextCurrent and wglMakeContextCurrent will accept Nil parameters for the drawables/HDCs. So you can do OpenGL operations on framebuffer objects and all the other stuff, with the only caveat being, that there's no main default framebuffer.

Feel free to use those code snippets for creating the helper contexts.

#include "wglu_context.h"
#include <windows.h>
#include <GL/gl.h>
#include <GL/wglext.h>

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <assert.h>

static char const *wgl_create_context_attribs_name = "wglCreateContextAttribsARB";

static
int wglu_get_proc_address(char const *name, PROC *pproc)
{
    int rc= 0;
    *pproc = wglGetProcAddress(name);
    fprintf(stderr, "%s: %p\n", name, (void*)*pproc );
    if( !(*pproc) ){
        rc= GetLastError();
        fprintf(stderr,
            "error wglGetProcAddress('%s'): 0x%x\n",
            name, rc);
    }
    if( rc ){ fprintf(stderr, "%s: %d\n", __func__, rc); }
    return rc;
}

/* -----------------------------------------------------------------------
 * Create a OpenGL context of desired version and share it */
int wglu_create_context_with_sharing(
    int major, int minor, int profile,
    HDC   surface,
    HGLRC share_context,
    HGLRC *out_context )
{
    int rc= 0;
    int attribs[8] = {0};
    size_t i_attrib = 0;
    HGLRC context = 0;

    HDC   const save_surface = wglGetCurrentDC();
    HGLRC const save_context = wglGetCurrentContext();
    if( save_surface != surface
     || save_context != share_context
    ){
        wglMakeCurrent(surface, share_context);
    }

    PFNWGLCREATECONTEXTATTRIBSARBPROC wgl_create_context_attribs;
    if( (rc= wglu_get_proc_address(
            wgl_create_context_attribs_name,
            (PROC*)&wgl_create_context_attribs))
    ){
        goto fail;
    }

    if( major ){
        attribs[i_attrib++] = WGL_CONTEXT_MAJOR_VERSION_ARB;
        attribs[i_attrib++] = major;
    }
    if( minor ){
        attribs[i_attrib++] = WGL_CONTEXT_MINOR_VERSION_ARB;
        attribs[i_attrib++] = minor;
    }
    if( profile ){
        attribs[i_attrib++] = WGL_CONTEXT_PROFILE_MASK_ARB;
        attribs[i_attrib++] = profile;
    }
    attribs[i_attrib] = attribs[i_attrib+1] = 0;

    context = wgl_create_context_attribs(surface, *share_context, attribs);
    if( !context ){
        rc= GetLastError();
        fprintf(stderr,
            "error %s(surface=0x%x, share_context=0x%x, attribs=0x%p): %x\n",
            wgl_create_context_attribs_name,
            (uintptr_t)surface, (uintptr_t)share_context,
            attribs,
            rc );
        goto fail;
    }

    if( !(wglMakeCurrent(surface, context)) ){
        rc= GetLastError();
        fprintf(stderr,
            "error %s(surface=0x%x, contest=0x%x): 0x%x\n",
            "wglMakeCurrent",
            (uintptr_t)surface, (uintptr_t)context,
            rc );
        goto fail;
    }
    assert( context == wglGetCurrentContext() );
    fprintf(stderr,
        "GL_VENDOR   = %s\n"
        "GL_RENDERER = %s\n"
        "GL_VERSION  = %s\n",
        glGetString(GL_VENDOR),
        glGetString(GL_RENDERER),
        glGetString(GL_VERSION) );

    if( !(wglMakeCurrent(NULL, NULL)) ){
        rc= GetLastError();
        fprintf(stderr,
            "error %s(0, 0): 0x%x\n",
            "wglMakeCurrent",
            (uintptr_t)surface, (uintptr_t)context,
            rc );
        goto fail;
    }
    if( !(wglShareLists(context, share_context)) ){
        rc= GetLastError();
        fprintf(stderr,
            "error %s(context=0x%x, share_context=0x%x): 0x%x\n",
            "wglShareLists",
            (uintptr_t)context, (uintptr_t)share_context,
            rc );
        goto fail;
    }

    wglMakeCurrent(save_surface, save_context);

    if( !rc ){
        if( out_context ){ *out_context = context; }
    } else {
        if( context ){ wglDeleteContext(context); }
    }

    if( rc ){ fprintf(stderr, "%s: %d\n", __func__, rc); }
    return rc;
}
#include <GL/glxext.h>

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <assert.h>
#include <errno.h>
#include <dlfcn.h>

static char const *glX_create_context_attribs_name = "glXCreateContextAttribsARB";

typedef void (*PROC)();

static
int glxu_get_proc_address(char const *name, PROC *pproc)
{
    int rc= 0;
    static PROC (*glX_get_proc_address)(char const*) = NULL;
    if( !glX_get_proc_address ){
        *(void**)(&glX_get_proc_address) = dlsym(RTLD_DEFAULT, "glXGetProcAddress");
    }
    if( !glX_get_proc_address ){
        rc = -EFAULT;
    } else {
        *pproc = glX_get_proc_address(name);
        fprintf(stderr, "%s: %p\n", name, (void*)*pproc );
        if( !(*pproc) ){
            rc= -ENOENT;
            fprintf(stderr, "error glXGetProcAddress('%s'): so such function\n", name);
        }
    }
    if( rc ){ fprintf(stderr, "%s: %d\n", __func__, rc); }
    return rc;
}

static
int glxu_get_fbconfig_for_xid(
    Display *display,
    GLXFBConfigID const id,
    GLXFBConfig *out_fbconfig )
{
    static GLXFBConfig* (*glX_get_fb_configs)(Display*,int,int*) = NULL;
    static int          (*glX_get_fb_config_attrib)(Display*,GLXFBConfig,int,int*) = NULL;
    if( !glX_get_fb_configs ){
        *(void**)(&glX_get_fb_configs) = dlsym(RTLD_DEFAULT, "glXGetFBConfigs");
    }
    if( !glX_get_fb_config_attrib ){
        *(void**)(&glX_get_fb_config_attrib) = dlsym(RTLD_DEFAULT, "glXGetFBConfigAttrib");
    }

    int rc = 0;
    int n_configs = 0;
    /* we always assume to operate on screen 0, since hardly any X connection
     * encountered these days actually operates on multiple screens. */
    if( !glX_get_fb_configs || !glX_get_fb_config_attrib ){
        rc = -EFAULT;
    } else {
        GLXFBConfig *const fbconfig = glX_get_fb_configs(display, 0, &n_configs);
        for(int i=0; !rc && i < n_configs; ++i){
            unsigned int qry_id;
            rc= glX_get_fb_config_attrib(display, fbconfig[i], GLX_FBCONFIG_ID, &qry_id);
            if( !rc && id == qry_id ){
                *out_fbconfig = fbconfig[i];
                break;
            }
        }
    }
    return rc;
}

Display *glxu_get_current_display(void)
{
    static Display* (*glX_get_current_display)(void) = NULL;
    if( !glX_get_current_display ){
        *(void**)(&glX_get_current_display) = dlsym(RTLD_DEFAULT, "glXGetCurrentDisplay");
    }
    if( !glX_get_current_display ){
        return NULL;
    }
    return glX_get_current_display();
}

GLXDrawable glxu_get_current_drawable(void)
{
    static GLXDrawable (*glX_get_current_drawable)(void) = NULL;
    if( !glX_get_current_drawable ){
        *(void**)(&glX_get_current_drawable) = dlsym(RTLD_DEFAULT, "glXGetCurrentDrawable");
    }
    if( !glX_get_current_drawable ){
        return 0;
    }
    return glX_get_current_drawable();
}

/* -----------------------------------------------------------------------
 * Create a OpenGL context of desired version and share it */
int glxu_create_context_with_sharing(
    int major, int minor, int profile,
    Display *display,
    GLXDrawable surface,
    GLXContext share_context,
    GLXContext *out_context )
{
    int rc= 0;
    int attribs[8] = {0};
    size_t i_attrib = 0;
    GLXContext context = 0;
    unsigned int fbconfigxid = 0;
    GLXFBConfig fbconfig = 0;

    static GLXContext  (*glX_get_current_context)(void)  = NULL;
    static void        (*glX_query_drawable)(Display*,GLXDrawable,int,unsigned*)  = NULL;
    static Bool        (*glX_make_current)(Display*,Drawable,GLXContext) = NULL;
    static void        (*glX_destroy_context)(Display*,GLXContext) = NULL;
    if( !glX_get_current_context ){
        *(void**)(&glX_get_current_context) = dlsym(RTLD_DEFAULT, "glXGetCurrentContext");
    }
    if( !glX_query_drawable ){
        *(void**)(&glX_query_drawable) = dlsym(RTLD_DEFAULT, "glXQueryDrawable");
    }
    if( !glX_make_current ){
        *(void**)(&glX_make_current) = dlsym(RTLD_DEFAULT, "glXMakeCurrent");
    }
    if( !glX_destroy_context ){
        *(void**)(&glX_destroy_context) = dlsym(RTLD_DEFAULT, "glXDestroyContext");
    }

    if( !glX_get_current_context || !glX_query_drawable
     || !glX_make_current        || !glX_destroy_context
    ){
        return -EFAULT;
    }

    Display     *const save_display = glxu_get_current_display();
    GLXDrawable  const save_surface = glxu_get_current_drawable();
    GLXContext   const save_context = glX_get_current_context();

    if( !display ){ display = save_display; }
    if( !surface ){ surface = save_surface; }
    if( !share_context ){ share_context = save_context; }

    if( save_display != display
     || save_surface != surface
     || save_context != share_context
    ){
        fprintf(stderr, ".....\n");
        if( !(glX_make_current(display, surface, share_context)) ){
            rc = -1;
            goto fail;
        }
    }

    PFNGLXCREATECONTEXTATTRIBSARBPROC glX_create_context_attribs;
    if( (rc= glxu_get_proc_address(
            glX_create_context_attribs_name,
            (PROC*)&glX_create_context_attribs))
    ){
        rc = -2;
        goto fail;
    }

    glX_query_drawable(display, surface, GLX_FBCONFIG_ID, &fbconfigxid);
    glxu_get_fbconfig_for_xid(display, fbconfigxid, &fbconfig);

    if( major ){
        attribs[i_attrib++] = GLX_CONTEXT_MAJOR_VERSION_ARB;
        attribs[i_attrib++] = major;
    }
    if( minor ){
        attribs[i_attrib++] = GLX_CONTEXT_MINOR_VERSION_ARB;
        attribs[i_attrib++] = minor;
    }
    if( profile ){
        attribs[i_attrib++] = GLX_CONTEXT_PROFILE_MASK_ARB;
        attribs[i_attrib++] = profile;
    }
    attribs[i_attrib] = attribs[i_attrib+1] = 0;

    context = glX_create_context_attribs(display, fbconfig, share_context, True, attribs);
    if( !context ){
        rc= -3;
        fprintf(stderr,
            "error %s(surface=0x%p, share_context=0x%p, attribs=0x%p): %x\n",
            glX_create_context_attribs_name,
            (void*)surface, (void*)share_context,
            attribs,
            rc );
        goto fail;
    }

    if( !(glX_make_current(display, surface, context)) ){
        rc= -4;
        fprintf(stderr,
            "error %s(surface=0x%p, contest=0x%p): 0x%x\n",
            "wglMakeCurrent",
            (void*)surface, (void*)context,
            rc );
        goto fail;
    }
    assert( context == glX_get_current_context() );
    fprintf(stderr,
        "GL_VENDOR   = %s\n"
        "GL_RENDERER = %s\n"
        "GL_VERSION  = %s\n",
        glGetString(GL_VENDOR),
        glGetString(GL_RENDERER),
        glGetString(GL_VERSION) );

fail:
    glX_make_current(display, save_surface, save_context);

    if( !rc ){
        if( out_context ){ *out_context = context; }
    } else {
        if( context ){ glX_destroy_context(display, context); }
    }

    if( rc ){ fprintf(stderr, "%s: %d\n", __func__, rc); }
    return rc;
}

Bool glxu_make_context_current(
    Display* display,
    GLXDrawable read, GLXDrawable draw,
    GLXContext ctx )
{
    static Bool (*glX_make_context_current)(Display*,Drawable,Drawable,GLXContext) = NULL;
    if( !glX_make_context_current ){
        *(void**)(&glX_make_context_current) = dlsym(RTLD_DEFAULT, "glXMakeContextCurrent");
    }
    if( !glX_make_context_current ){
        return False;
    }
    return glX_make_context_current(display, read, draw, ctx);
}