Search code examples
openglqt5vbovao

QOpenGLVertexArrayObject causes segfault with multiple VBOs?


I'm trying to use instanced rendering in Qt 5, and I'm having some trouble. I think I've traced my problems back to my use of VAOs. I'm an experienced enough programmer to know that these problems are almost always my fault, but I've run into the occasional Qt bug, so I thought I could use some help before filing a bug report.

I've put together a simple example, which doesn't use instancing (for some reason I can't request a recent version of OpenGL, even though I don't have this problem in the "real" software I'm developing).

At initialization time, I create a VAO, and two VBOs. One VBO is filled with some data, and remains bound; The other is allocated, but released (I never actually use it: I was planning to eventually write matrices to it for instancing, but didn't get that far).

I've added some preprocessor directives to simplify turning usage of the VAO or the second VBO on or off. If I use both the VAO and both VBOs, I get a segmentation fault. If I get rid of either the second VBO or the VAO, there's no seg fault and the drawing looks right (it should be a white triangle drawn behind a coloured triangle).

Here's the code:

glbuffertest.cpp:

######################################################################
# Automatically generated by qmake (3.0) Fri May 16 09:49:41 2014
######################################################################

TEMPLATE = app
TARGET = glbuffertest
INCLUDEPATH += .

CONFIG += qt debug

DEFINES += USE_VAO 
DEFINES += USE_MATBO

# Input
SOURCES += glbuffertest.cpp glwindow.cpp
HEADERS += glwindow.h

glbuffertest.cpp:

#include <QGuiApplication>
#include <QSurfaceFormat>

#include "glwindow.h"



int main(int argc, char **argv)
{

  QGuiApplication app(argc, argv);

  GLWindow window;  
  window.resize(400, 400);
  window.show();

  return app.exec();
}

glwindow.cpp:

#include "glwindow.h"

#include <QColor>
#include <QMatrix4x4>
#include <QVector>
#include <QVector3D>
#include <QVector4D>

#include <QDebug>


GLWindow::GLWindow(QWindow *parent) 
  : QWindow(parent)
  , _vbo(QOpenGLBuffer::VertexBuffer)
  , _matbo(QOpenGLBuffer::VertexBuffer)
  , _context(0)
{
  setSurfaceType(QWindow::OpenGLSurface);
  create();
}

GLWindow::~GLWindow()
{}

void GLWindow::initGL()
{
  if (_context) // already initialized
    return;

  _context = new QOpenGLContext(this);
  QSurfaceFormat format(requestedFormat());
  format.setDepthBufferSize(24);

  _context->setFormat(format);
  _context->create();
  _context->makeCurrent(this);

  setupShaders();
  _program->bind();
  _positionAttr = _program->attributeLocation("position");
  _colourAttr = _program->attributeLocation("colour");
  _matrixUnif = _program->uniformLocation("matrix");
  _program->release();

  initializeOpenGLFunctions();

  QVector<QVector3D> triangles;
  triangles << QVector3D(-0.5, 0.5, 1) << QVector3D(-0.5, -0.5, 1) << QVector3D(0.5, -0.5, 1);
  triangles << QVector3D(0.5, 0.5, 0.5) << QVector3D(-0.5, -0.5, 0.5) << QVector3D(0.5, -0.5, 0.5);

  QVector<QVector3D> colours;
  colours << QVector3D(1, 0, 0) << QVector3D(0, 1, 0) << QVector3D(0, 0, 1);
  colours << QVector3D(1, 1, 1) << QVector3D(1, 1, 1) << QVector3D(1, 1, 1);

#ifdef USE_VAO

  _vao.create();
  _vao.bind();

#endif  
  _vbo.create();
  _vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
  _vbo.bind();

  size_t positionSize = triangles.size() * sizeof(QVector3D);
  size_t colourSize = colours.size() * sizeof(QVector3D);
  _vbo.allocate(positionSize + colourSize);
  _vbo.write(0, triangles.constData(), positionSize);
  _vbo.write(positionSize, colours.constData(), colourSize);
  _colourOffset = positionSize;

#ifdef USE_MATBO
  _matbo.create();
  _matbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
  _matbo.bind();

  _matbo.allocate(4 * sizeof(QVector4D));
  qDebug() << "matrix buffer size:" << _matbo.size() << "requested:" << 4 * sizeof(QVector4D);
  _matbo.release();
#endif

#ifdef USE_VAO
  glVertexAttribPointer(_positionAttr, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glVertexAttribPointer(_colourAttr, 3, GL_FLOAT, GL_FALSE, 0, (void*)(_colourOffset));

  glEnableVertexAttribArray(_positionAttr);  
  glEnableVertexAttribArray(_colourAttr);
  _vao.release();
#else
  _vbo.release();
#endif

  resizeGL(width(), height());
}

void GLWindow::resizeGL(int w, int h)
{
  glViewport(0, 0, w, h);
}

void GLWindow::paintGL()
{
  if (! _context) // not yet initialized
    return;

  _context->makeCurrent(this);
  QColor background(Qt::black);

  glClearColor(background.redF(), background.greenF(), background.blueF(), 1.0f);
  glClearDepth(1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  QMatrix4x4 matrix;
  matrix.perspective(60, 4.0/3.0, 0.1, 100.0);
  matrix.translate(0, 0, -2);

  _program->bind();

  _program->setUniformValue(_matrixUnif, matrix);

#ifdef USE_VAO
  _vao.bind();

#else
  _vbo.bind();
  glVertexAttribPointer(_positionAttr, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glVertexAttribPointer(_colourAttr, 3, GL_FLOAT, GL_FALSE, 0, (void*)(_colourOffset));

  glEnableVertexAttribArray(_positionAttr);  
  glEnableVertexAttribArray(_colourAttr);
#endif

  glEnable(GL_DEPTH_TEST);

  glDrawArrays(GL_TRIANGLES, 0, 6);

#ifdef USE_VAO
  _vao.release();
#else
  glDisableVertexAttribArray(_positionAttr);
  glDisableVertexAttribArray(_colourAttr);
  _vbo.release();

#endif
  _program->release();

  _context->swapBuffers(this);
  _context->doneCurrent();

}

static const char *vertexShaderSource =
    "attribute highp vec4 position;\n"
    "attribute lowp vec4 colour;\n"
    "uniform highp mat4 matrix;\n" 
    "varying lowp vec4 col;\n"
    "void main() {\n"
    "   col = colour;\n"
    "   gl_Position = matrix * position;\n"
    "}\n";

static const char *fragmentShaderSource =
    "varying lowp vec4 col;\n"
    "void main() {\n"
    "   gl_FragColor = col;\n"
    "}\n";

void GLWindow::setupShaders()
{

  _program = new QOpenGLShaderProgram(this);
  _program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
  _program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
  _program->link();


}

void GLWindow::exposeEvent(QExposeEvent *event)
{
  Q_UNUSED(event);

  if (isExposed())
  {
    if (! _context)
      initGL();

    paintGL();
  }
}

glwindow.h:

#ifndef GL_WINDOW_H
#define GL_WINDOW_H

#include <QExposeEvent>
#include <QSurfaceFormat>
#include <QWindow>

#include <QOpenGLBuffer>
#include <QOpenGLContext>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>


class GLWindow : public QWindow, protected QOpenGLFunctions
{
  Q_OBJECT
public:
  GLWindow(QWindow * = 0);
  virtual ~GLWindow();

  void initGL();
  void paintGL();
  void resizeGL(int, int);

protected:
  virtual void exposeEvent(QExposeEvent *);


private:

  void setupShaders();

  QOpenGLBuffer _vbo;
  QOpenGLBuffer _matbo;
  QOpenGLContext *_context;
  QOpenGLShaderProgram *_program;
  QOpenGLVertexArrayObject _vao;

  GLuint _positionAttr;
  GLuint _colourAttr;
  GLuint _matrixUnif;

  size_t _colourOffset;

} ; 

#endif

Can anyone spot something stupid I'm doing?


Solution

  • The obvious GL problem is in your glVertexAttribPointer calls.

    glVertexAttribPointer sets generic vertex attribute data. Its behaviour changes depending on whether a buffer is currently bound on the GL_ARRAY_BUFFER binding.

    Yes, that's horrible.

    And since this is the primary usage of buffers bound to that binding point, that's why they get called vertex buffer objects.

    If there is a buffer bound, then it configures that attribute to read the data from that buffer, and the last argument gets interpreted as an offset in bytes from the beginning of the buffer. The actual amount of data that will be read read depends on the other arguments (count, size of the datatype, stride).

    If there is not a buffer bound at that binding point, then it reads data from the memory location (in your program's address space) pointed from the last argument. Again, the amount of data that gets read from main memory and transferred over to GL depends on the other arguments.

    In your case there's nothing bound: you just released whatever was bound to GL_ARRAY_BUFFER. Yes, those buffer points don't "stack up" or anything. There can be at most one buffer bound at that binding point.

    When you call

    _matbo.release();
    

    you're telling GL to drop any binding to the GL_ARRAY_BUFFER binding point. Hence, these calls:

    glVertexAttribPointer(_positionAttr, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glVertexAttribPointer(_colourAttr, 3, GL_FLOAT, GL_FALSE, 0, (void*)(_colourOffset));
    

    are telling GL to read vertex attribute data from main memory, and I'm pretty sure there's nothing useful at the memory addresses 0 and _colourOffset. Expect a crash very soon...

    The solution? Just rebind the right buffer object to the binding point before calling glVertexAttribPointer:

    _vbo.bind();
    glVertexAttribPointer(_positionAttr, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glVertexAttribPointer(_colourAttr, 3, GL_FLOAT, GL_FALSE, 0, (void*)(_colourOffset));
    

    which is exactly what you do in your working non-VAO path.


    Other suggestions:

    1. Since you're already using QOpenGLShaderProgram, you could also just use its functions:

      program->setAttributeBuffer(...);
      
    2. you should be very careful at uploading in buffers the representation of Qt types such as QVector3D or QMatrix4x4 to OpenGL. You have no control over their in-memory representation. You should either use some Qt wrappers, or upload raw data (to be 100% sure).

    3. you should either ask for a specific (minimum) GL version, or at least test that the VAO creation is successful (create returning true) before using it. VAOs don't exist for instance on GL 2 or ES 2, but require extensions.