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