Defining Buffer layouts with StructuredBufferView

Calculating buffer layouts

When a UBO or SSBO interface block is defined in the shader, and the developer needs to fill it with data, strictly follow the STD140 (or STD430) GLSL rules to determine the memory layout, bit for bit, including paddings. Then translate that into a C++ layout or manually memcpy every bit of it into the mapped block.

This can become extremely tedious, especially when considering potential inner structs or other similar complications. Fortunately, PVRUtils is able to help with this.

The StructuredBufferView

This class takes a tree-structure definition of entries, automatically calculates their offsets based on std140 rules (an std430 version is planned), and allows utilities to directly set values into mapped pointers.

Note: The ease that this provides cannot be overstated – normally it would be necessary to go through all the std140 ruleset and determine the offset manually for every case of setting a value into a buffer.

This is a code example from the Skinning SDK example:

GLSL
struct Bone 
{
    highp mat4 boneMatrix;
    highp mat3 boneMatrixIT;
}; // SIZE: 4x16 + 3x16(!) = 112. Alignment: Must align to 16 bytes 
            
layout (std140, binding = 0) buffer BoneBlock 
{
    mediump int BoneCount; // OFFSET 0, size 4
    Bone bones[]; // starts at 16, then 112 bytes each element
}; 
CPU side

An easy-to-use interface is provided to define the StructuredBufferView. Using C++ initialiser lists, a compact JSON-like constructor has been created that allows the developer to easily express any structure.

The following code fragment shows the corresponding CPU-side code for the GLSL above:

// LAYOUT OF THE BUFFERVIEW
pvr::utils::StructuredMemoryDescription descBones("Ssbo", 1, // 1: The UBO itself is not array
{
    { "BoneCount", pvr::GpuDatatypes::Integer } // One integer element, name “BoneCount”
    { // One element, name “Bones”, that contains...
        "Bones", 1, 
        { // One mat4x4 and one mat3x3
              {"BoneMatrix", pvr::GpuDatatypes::mat4x4},
              {"BoneMatrixIT", pvr::GpuDatatypes::mat3x3}
        }
    }
});
                
// CREATING THE BUFFERVIEW
pvr::utils::StructuredBufferView ssboView;
ssboView.init(descBones); // One-shot initialisation to avoid mistakes.
                
// SETTING VALUES
void* bones = gl::MapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, ssboView.getSize(), GL_MAP_WRITE_BIT);
int32_t boneCount = mesh.getNumBones();
ssboView.getElement(_boneCountIdx).setValue(bones, &boneCount);
auto root = ssboView.getBufferArrayBlock(0);
                
for (uint32_t boneId = 0; boneId < numBones; ++boneId)
{
    const auto &bone = _scene->getBoneWorldMatrix(nodeId, mesh.getBatchBone(batch, boneId));
    auto bonesArrayRoot = root.getElement(_bonesIdx, boneId);
    bonesArrayRoot.getElement(_boneMatrixIdx).setValue(bones, 
    glm::value_ptr(bone));
    bonesArrayRoot.getElement(_boneMatrixItIdx).setValue(bones,
                                glm::value_ptr(glm::inverseTranspose(bone))));
}

gl::UnmapBuffer(GL_SHADER_STORAGE_BUFFER);

It is highly recommended to give the StructuredBufferView a try even if there is no intention to use the rest of the Framework.