New computeStyle builder

There is a new way to create compute shaders: the computeStyle builder. It follows a pattern similar to that of shadeStyle.

A few fixes landed in this code last week, so I can now share a small example of how it can simplify the creation of compute shader programs that use SSBO’s and structs. Note: only if you are building openrndr yourself since it’s not yet published to maven.

Let’s assume we have a compute shader program like this:

imports
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
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(1.0.toFloat()) // age
                write(Vector3(2.1, 2.2, 2.3))// pos
            }
        }

        // GLSL shader code
        @Language("GLSL")
        val glsl = """
            #version 430
            layout(local_size_x = 1, local_size_y = 1) in;
            
            struct Particle {
              float age;
              vec3 pos;
            };
            
            // 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, "compute02")
        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
        byteBufferBeforeExecute.rewind()
        byteBufferAfterExecute.rewind()
        repeat(ssbo.format.size / 4) {
            println("$it: ${byteBufferBeforeExecute.float} -> ${byteBufferAfterExecute.float}")
        }
    }
}

This program is just for testing purposes, as it doesn’t do any massively parallel computation, nor produce any graphical output, but just alter a few numbers to verify that id works as expected.

Look at the glsl variable: it declares a bunch of required boilerplate. By using the shadeStyle { } builder, we can just keep what’s inside the GLSL main() function, and discard the rest, which gets generated automatically. The simplified code looks like this:

imports
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
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(1.0.toFloat()) // age
                write(Vector3(2.1, 2.2, 2.3))// pos
            }
        }

        // Create Compute Shader
        val cs = computeStyle {
            computeTransform = """
                b_myData.time = 3.3;
                b_myData.vertex[0] = vec2(7.01);
                b_myData.vertex[1] = vec2(7.02);
                b_myData.vertex[2] = vec2(7.03);
                b_myData.particles[0].pos = vec3(112.0);
                b_myData.particles[0].age = 111.0;                
            """.trimIndent()
        }
        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
        byteBufferBeforeExecute.rewind()
        byteBufferAfterExecute.rewind()
        repeat(ssbo.format.size / 4) {
            println("$it: ${byteBufferBeforeExecute.float} -> ${byteBufferAfterExecute.float}")
        }
    }
}

Note how we use b_myData. This is because the SSBO gets a name specified when calling cs.buffer(...). When using only one SSBO naming it is not necessary, but it’s good practice as it shows where the data came from (a buffer) and makes it easy to use multiple such buffers, each with a unique name.

Thanks to the shadeStyle feature, the updated code is 21 lines shorter, and more importantly, there are fewer opportunities for typos by not having to declare the struct or the buffer layout yourself in the GLSL code.

Note: to specify the workgroup size we can use the following: cs.workGroupSize = IntVector3(32, 32, 1) with whatever values we consider proper.

Time to play with this :slight_smile:

Several demos which at some point I may turn into videos, tutorials or a workshop:

1 Like