Vulkan-based graphics library to improve developer efficiency

I’m considering the idea of a higher-but-still-pretty-low-level library to reduce the time and cost of Vulkan development. The basic problem, as I see it, is this:

  • Application code is complex
  • Vulkan code is complex
  • Trying to interface these two things directly is extremely time-consuming and results in a large amount of complicated and brittle code.

I believe an intermediate graphics library, perhaps modeled after OpenGL’s API style, would greatly improve programmer efficiency with no loss of performance and allow more agile application development. WebGPU and ANGLE are proof this approach can work well, but unfortunately those libraries are both “lowest common denominator” abstraction layers that each offer less functionality than even OpenGL 4.6.

I am curious to hear if other developers around here recognize a need for reducing development time and costs with something like this? I have a lot of experience with Khronos graphics APIs and could probably write the whole thing myself, but would prefer to collaborate with others if there is a general need for this.

My general concept right now is like a cleaned-up version of OpenGL 4.6, with the following additions:

  • glInstance objects
  • glPipeline objects
  • glCommandBuffer objects (probably this is how multithreading would work, although I would not force use of them)
  • Raytracing support
  • Lots of additional error checking in debug builds

I think it would be possible to make an example of a spinning cube with raytraced shadows in less than 150 lines of code, as well as more complex applications that would be indistinguishable from a pure Vulkan app.

Here is some concept code I wrote down:

void RunExample(HWND hwnd, void* texturedata, int texsize, void* vertshader, GLSizei vertshadersize, void* fragshader, GLSizei fragshadersize)
{
    // Initialize instance
    GLinstance inst = glCreateInstance();

    // Select GPU
    GLuint count;
    GLDevice *device;
    glGetDevices(inst, &count, &device);
    GLDeviceSelectInfo dinfo;// constructor fills in default values
    dinfo.extensions.bindlesstextures.enabled = true;// enable an extension
    glSelectDevice(inst, deviceid[0], dinfo);// discrete GPU always comes first
    printf(glGetDeviceString(deviceid[0], GL_VENDOR));
    printf(glGetDeviceString(deviceid[0], GL_RENDERER));

    // Create framebuffer
    GLFramebuffer framebuffer = glCreateFramebuffer(inst, hwnd);

    // Create vertex shader module
    GLShader vertshader = glCreateShader(inst, GL_VERTEX_SHADER);
    glShaderBinary(1, &vertshader, GL_SHADER_BINARY_FORMAT_SPIR_V, vertshader, vertshadersize);
    glSpecializeShader(vertshader, "main", 0, 0, 0);

    // Create fragment shader module
    GLShader fragshader = glCreateShader(inst, GL_FRAGMENT_SHADER);
    glShaderBinary(1, &fragshader, GL_SHADER_BINARY_FORMAT_SPIR_V, fragshader, fragshadersize);
    glSpecializeShader(fragshader, "main", 0, 0, 0);

    // Create shader program
    GLProgram program = glCreateProgram(inst);
    glAttachShader(program, vertshader);
    glAttachShader(program, fragshader);
    glLinkProgram(program);
    glValidateProgram(program);

    // Create vertex buffer
    const int vertexstride = 5 * 4;// position x3 + texcoords x2
    GLVertexBuffer vertbuffer = glCreateVertexBuffer(inst);
    float vertexdata[24 * 5] = { 0.0f, 0.0f, 0.0f };
    glVertexBufferSetData(vertbuffer, &vertexdata[0], 24 * vertexstride);

    // Create indice buffer
    GLElementBuffer indicebuffer = glCreateElementBuffer(inst);
    unsigned short indices[36] = { 0, 1, 2, 2, 3, 0 };
    glElementBufferSetData(indicebuffer, &indices[0], 36 * 2, GL_UINT16);

    // Create texture
    GLTexture texture = glCreateTexture(inst);
    glTextureStorage2D(texture, 1, GL_R8G8B8A8_UNORM, texsize, texsize);
    glTextureSubImage2D(texture, 0, 0, 0, texsize, texsize, GL_R8G8B8A8_UNORM, texturedata);
    glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    // Create pipeline - this is just a container for draw settings, not a 1:1 mapping to Vulkan pipelines
    GLPipeline pipeline = glCreatePipeline(inst);
    
    // Set pipeline attributes
    glSetPipelineAttribute(pipeline, 0, 3, GL_FLOAT, GL_FALSE, vertexstride, vertexbuffer);
    glSetPipelineElementBuffer(pipeline, 0, 3, GL_FLOAT, GL_FALSE, vertexstride, indicebuffer);
    
    while (true)
    {
        //Clear the frame buffer
        GLfloat cleardepth = 1.0f;
        GLfloat clearcolor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
        glClearFramebufferfv(framebuffer, GL_DEPTH, 0, &cleardepth);
        glClearFramebufferfv(framebuffer, GL_COLOR, 0, &clearcolor[0]);

        // Prepare the pipeline
        glSetPipelineFramebuffer(pipeline, framebuffer);
        glSetPipelineShader(pipeline, program);
        glBindPipelineTextures(pipeline, 1, &texture);
        glPipelineParameteri(pipeline, GL_BLEND, GL_FALSE);// instead of glDisable(GL_BLEND)

        // Set vertex buffers for position and texcoords
        glPipelineVertexAttrib(pipeline, 0, 3, GL_FLOAT, GL_FALSE, vertexstride, 0, vertexbuffer);
        glPipelineVertexAttrib(pipeline, 1, 2, GL_FLOAT, GL_FALSE, vertexstride, 12, vertexbuffer);
        glEnablePipelineVertexAttribArray(pipeline, 0);
        glEnablePipelineVertexAttribArray(pipeline, 1);

        // Enable the indice buffer
        glEnablePipelineElementArray(pipeline);

        // Draw the mesh
        glDrawElements(pipeline, GL_TRIANGLES, 36, GL_UINT16, 0);

        // Pipeline cleanup
        glDisablePipelineVertexArrayAttrib(pipeline, 0);
        glDisablePipelineElementArray(pipeline);

        // Swap buffers
        glSwapBuffer(framebuffer);
    }
}

Please let me know if this idea interests you.

Are they, though? Are there high-performance programs built on these tools that can match the CPU performance of equivalent Vulkan programs?

That right there is where you’re going to run into a big problem. There’s a reason why NVIDIA, despite clearly loving OpenGL, does not expose raytracing support through OpenGL. It’s because they can’t. The structure of OpenGL’s API model is just inappropriate to doing the asynchronous stuff that ray tracing needs.

For ray tracing, command buffers and explicit synchronization are not optional.

Here is some concept code I wrote down:

Why are framebuffers, buffer objects, and textures instance objects instead of device objects? I also don’t see where you set up any of the data needed to do ray tracing. It’s not just something that happens in your shader; it fundamentally changes the very structure of how you render.

Also, I don’t see how the API you’ve described is in any way different from OpenGL. It would be subject to all of the issues that make OpenGL not forward-looking and slow in terms of CPU activity.

At the end of the day, any kind of “Vulkan but higher level” API is probably best done as a set of helper libraries that provide simple solutions to some of Vulkan’s complexity. A library for managing swapchains and dealing with window resizing and the like. A library for managing memory, dealing with allocations but allowing you to have a lot of control over how all of that works. Etc.

I am more interested in coming up with constructive solutions than arguing about the details of Vulkan, but the more I dig into Vulkan the more I find that reality does not match popular opinion. For my purposes, the increased complexity of Vulkan has not offered drastic performance gains over performant OpenGL code. Both my VK and GL renderers are much faster than almost anything else out there, so it’s not just a bad implementation. Based on my experience I do not believe the idea that simplifying the Vulkan developer experience will make performance slower.

I can’t speak to the details of raytracing because that is one area I have not gone into in detail, but I expect there will probably be a simple and elegant solution.