Compute Shaders and buffers

Hallo there,
trying to find my way with compute shaders in Openrndr (I have experience with them in Unity), but the guide is particularly dense on the subject. In particular, is there a way to send/bind a general type buffer to the GPU? From the API I can see that the .buffer method of the compute shader class accepts “ShaderStorageBuffer” as well, but I have not been able to create them.
I guess the origin of the question is that encapsulating data in VertexBuffer obejcts can be quite limiting in the type of structs you can declare in the compute shader itself. Am I missing something?

I think this thread is of interest.

Thanks, I am aware of that thread: that is actually where my question originated from in the first place :slight_smile:

Courtesy of @abe for pointing it, this provides help on ShaderStorageBuffers and how to implement them

Hey! Great to see you yesterday @Alessandro :slight_smile:

I will share related code and update the guide when I have a moment.

Actually, it would be great to show you my compute shader “framework” which I used in several projects to receive some feedback or to give ideas. Maybe you find it useful. I find it good to reuse some kind of pattern to create projects using compute shaders, since they often run multiple passes, they have a gui, SSBOs etc.

Maybe the first OPENRNDR meetup in Berlin would be a good place to talk about that. @kazik suggested we meet at his studio. We just need to find a date :slight_smile:

Hey @abe :slight_smile:

Actually, it would be great to show you my compute shader “framework” which I used in several projects to receive some feedback or to give ideas. Maybe you find it useful. I find it good to reuse some kind of pattern to create projects using compute shaders, since they often run multiple passes, they have a gui, SSBOs etc.

Yes, that’s a great idea, I can surely provide feedback while learning! :slight_smile:

Maybe the first OPENRNDR meetup in Berlin would be a good place to talk about that. @kazik suggested we meet at his studio. We just need to find a date :slight_smile:

I’m definitely up for it, let’s make this happen!

Why are all the cool stuff happening in Berlin. Would love to join but live in Saarbrücken :see_no_evil:

I just opened this chat with the intention of asking you if you are in Berlin @TSAO when I read your comment! A pity that we are so far, but maybe we can organize something later :slight_smile:

1 Like

I’ve been writing a series of programs (7 so far) using SSBO’s, with the goal of making the guide more complete and for practicing before recording new videos with Alessandro.

I’ll share here the first of such programs:

import org.intellij.lang.annotations.Language
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
 * SSBO -> https://www.khronos.org/opengl/wiki/Shader_Storage_Buffer_Object
 *
 * This program demonstrates
 * - how to create an SSBO
 * - how to populate an SSBO with data
 * - how to pass an SSBO to a compute shader
 * - how to download the SSBO data to the CPU before and after executing the compute shader
 *
 * It prints the before/after data to observe how it was modified by the compute shader.
 *
 * Writing a useful compute shader that processes data faster than in the CPU is NOT a goal
 * of this program, since such a simple calculation would be faster and easier if done completely in the CPU.
 *
 * Notice how the execute() call has a width, height and depth of 1
 * (basically doing just one computation).
 *
 * A useful compute shader would do a large number of parallel computations.
 * This will be presented in a different demo.
 */

fun main() = application {
    program {
        // Define SSBO format
        val fmt = shaderStorageFormat {
            primitive("time", BufferPrimitiveType.FLOAT32)
            primitive("vertex", BufferPrimitiveType.VECTOR2_FLOAT32, 3)
            struct("Particle", "particles", 5) {
                primitive("pos", BufferPrimitiveType.VECTOR3_FLOAT32)
                primitive("age", BufferPrimitiveType.FLOAT32)
            }
        }
        println("Study the padding in the format:\n$fmt\n")

        // Create SSBO
        val ssbo = shaderStorageBuffer(fmt)

        // Populate SSBO
        ssbo.put {
            write(3.0.toFloat()) // time
            repeat(3) {
                write(Vector2(1.1, 1.2)) // vertex
            }
            repeat(5) {
                write(Vector3(2.1, 2.2, 2.3))// pos
                write(1.0.toFloat()) // age
            }
        }

        // GLSL shader code
        @Language("GLSL")
        val glsl = """
            #version 430
            layout(local_size_x = 1, local_size_y = 1) in;
            
            struct Particle {
              vec3 pos;
              float age;
            };
            
            // A buffer using variables of different types:
            // a float, an array of vec2 and an array of struct.
            // These variable become available as globals.
            layout(std430, binding=0) buffer myData {
                float time;
                vec2 vertex[3];
                Particle particles[5];
            };
            
            void main() {                
                time = 3.3;
                vertex[0] = vec2(7.01);
                vertex[1] = vec2(7.02);
                vertex[2] = vec2(7.03);
                particles[0].pos = vec3(112.0);
                particles[0].age = 111.0;
            }
        """.trimIndent()
        
        // Create Compute Shader
        val cs = ComputeShader.fromCode(glsl, "compute01")
        cs.buffer("myData", ssbo)

        // Download SSBO data to CPU
        val byteBufferBeforeExecute = ByteBuffer.allocateDirect(ssbo.format.size).order(ByteOrder.nativeOrder())
        byteBufferBeforeExecute.rewind()
        ssbo.read(byteBufferBeforeExecute)

        // Execute compute shader
        cs.execute(1, 1, 1)

        // Download SSBO data to CPU
        val byteBufferAfterExecute = ByteBuffer.allocateDirect(ssbo.format.size).order(ByteOrder.nativeOrder())
        byteBufferAfterExecute.rewind()
        ssbo.read(byteBufferAfterExecute)

        // Debugging

        // Notice the (maybe unexpected) 0.0 padding values printed on the console.
        // Depending on the variable size in bytes, padding may be added by the system
        // to align them in memory. This will depend on the sizes of the involved variables,
        // and their order. For instance, a vec3 and a float do not require padding, but
        // a float followed by a vec3 pads the float with 3 values, and the vec3 with one.
        // Run compute02.kt and study the output to observe a more inefficient layout.
        byteBufferBeforeExecute.rewind()
        byteBufferAfterExecute.rewind()
        repeat(ssbo.format.size / 4) {
            println("$it: ${byteBufferBeforeExecute.float} -> ${byteBufferAfterExecute.float}")
        }
    }
}

This is the output of the program:

Study the padding in the format:
ShaderStorageFormat{items=[
  ShaderStoragePrimitive(name=time, type=FLOAT32, arraySize=1, offset=4, padding=4), 
  ShaderStoragePrimitive(name=vertex, type=VECTOR2_FLOAT32, arraySize=3, offset=24, padding=0), 
  ShaderStorageStruct(structName=Particle, name=particles, elements=[
    ShaderStoragePrimitive(name=pos, type=VECTOR3_FLOAT32, arraySize=1, offset=12, padding=0), 
    ShaderStoragePrimitive(name=age, type=FLOAT32, arraySize=1, offset=4, padding=0)
  ], 
  arraySize=5, offset=0)
], formatSize=112}

0: 3.0 -> 3.3
1: 0.0 -> 0.0
2: 1.1 -> 7.01
3: 1.2 -> 7.01
4: 1.1 -> 7.02
5: 1.2 -> 7.02
6: 1.1 -> 7.03
7: 1.2 -> 7.03
8: 2.1 -> 112.0
9: 2.2 -> 112.0
10: 2.3 -> 112.0
11: 1.0 -> 111.0
12: 2.1 -> 2.1
13: 2.2 -> 2.2
14: 2.3 -> 2.3
15: 1.0 -> 1.0
16: 2.1 -> 2.1
17: 2.2 -> 2.2
18: 2.3 -> 2.3
19: 1.0 -> 1.0
20: 2.1 -> 2.1
21: 2.2 -> 2.2
22: 2.3 -> 2.3
23: 1.0 -> 1.0
24: 2.1 -> 2.1
25: 2.2 -> 2.2
26: 2.3 -> 2.3
27: 1.0 -> 1.0