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; }