Qt compute shader without graphics

So I thought I would separate the generation of my content (terrain) from the display so that I could cache the content and use it to feed downstream computations without having to recompute the whole chain of operations every time. It tends to get assumed that if you’re using a compute shader you’re going to send the output to an OpenGL graphics pipeline, and actually only recently has it become easy to put data on the graphics card, process it there in a GLSL compute shader and bring the results back. So here’s an example.

Header file

class ComputeService {
public:
    ComputeService();
    ~ComputeService();
    float* compute (Graph* g, float* buffer, int size);

private:
    QString _sourceCode;
    QOffscreenSurface _surface;
    QOpenGLContext _context;
    QOpenGLShader* _computeShader;
    QOpenGLShaderProgram* _computeProgram;

    void execute (float* buffer, int height);
};

Source file

#include <iostream>
#include <src/CalenhadServices.h>
#include "ComputeService.h"

ComputeService::ComputeService () : _computeProgram (nullptr), _computeShader (nullptr) {
    // read code from files into memory
    QFile csFile (":/shaders/compute.glsl");
    csFile.open (QIODevice::ReadOnly);
    QTextStream csTextStream (&csFile);
    _sourceCode = csTextStream.readAll();
    
    // Create an  OpenGL context - in a graphics pipeline
    // we would inherit this with our QOpenGLWidget
    QSurfaceFormat format;
    format.setMajorVersion(4);
    format.setMinorVersion(3);
    format.setProfile(QSurfaceFormat::CoreProfile);
    _context.setFormat(format);
    if (!_context.create())
        throw std::runtime_error("context creation failed");
    _surface.create();
    _context.makeCurrent( &_surface);

    // initialise OpenGL on the context
    QOpenGLFunctions_4_3_Core openglFunctions;
    if (!openglFunctions.initializeOpenGLFunctions())
        throw std::runtime_error("initialization failed");
}

ComputeService::~ComputeService () {
    delete _computeShader;
    delete _computeProgram;
}

// Call this with a pointer to the buffer in which you
// want the results saved
float* ComputeService::compute (float* buffer, int size) {
    _context.makeCurrent( &_surface);
    delete _computeShader;
    delete _computeProgram;
    _computeShader = new QOpenGLShader (QOpenGLShader::Compute);
    _computeProgram = new QOpenGLShaderProgram ();
    clock_t start = clock ();

    QString code = g -> glsl ();
    if (code != QString::null) {
        if (_computeShader) {
            _computeProgram -> removeAllShaders ();
            if (_computeShader -> compileSourceCode (_sourceCode)) {
                _computeProgram -> addShader (_computeShader);
                _computeProgram -> link ();
                _computeProgram -> bind ();
                
                // launch the compute shader
                execute (buffer, size);
            } else {
                std::cout << "Compute shader would not compile\n";
            }
        }
    } else {
        std::cout << "No code for compute shader\n";
    }
}

// this handles the launch and fetches results
void ComputeService::execute (float* buffer, int size) {
    _context.makeCurrent (&_surface);
    GLuint ssbo;
    int bytes = size * sizeof (GLfloat);

    QOpenGLFunctions_4_3_Core* f = dynamic_cast<QOpenGLFunctions_4_3_Core*> (_context.versionFunctions ());
    f -> glUseProgram (_computeProgram -> programId ());
    
    // reserve space on the GPU
    f -> glGenBuffers (1, &heightMap);
    f -> glBindBuffer (GL_SHADER_STORAGE_BUFFER, ssbo);
    f -> glBufferData (GL_SHADER_STORAGE_BUFFER, bytes, NULL, GL_DYNAMIC_READ);
    f -> glBindBufferBase (GL_SHADER_STORAGE_BUFFER, 0, ssbo);
    // you'll want to tweak the geometry to the size of 
   // the buffer and its layout
    f -> glDispatchCompute (32, 64, 1);
    f -> glMemoryBarrier (GL_SHADER_STORAGE_BARRIER_BIT);

    // this gets the data from the GPU
    f -> glGetBufferSubData (GL_SHADER_STORAGE_BUFFER, 0, bytes, buffer);
    f -> glUnmapBuffer (GL_SHADER_STORAGE_BUFFER);
}

And the shader code itself will look like this – where value (ivec2 pos) is a function that computes the output value for a given invocation. Of course you will need different geometry!

#version 430
layout(local_size_x = 32, local_size_y = 32) in;
layout (std430, binding = 0) buffer out { float buffer_out []; };


void main() {
        ivec2 pos = ivec2 (gl_GlobalInvocationID.yx);
        float v = value (pos);
        buffer_out [pos.y * 32 * 32 * 2 + pos.x] = v;
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s