Măcelaru Tiberiu
Măcelaru Tiberiu

Reputation: 345

C++ Syntax for variable length array to be sent to GLSL in Vulkan

So I've read about Storage Buffers being able to contain a variable length array at end:

SSBOs can have variable storage, up to whatever buffer range was bound for that particular buffer; UBOs must have a specific, fixed storage size. This means that you can have an array of arbitrary length in an SSBO (at the end, rather). The actual size of the array, based on the range of the buffer bound, can be queried at runtime in the shader using the length function on the unbounded array variable

I know how to pass a storage buffer as a struct with simple fields like this:

struct GPUStorage {
   glm::vec3 mesh_pos;
   glm::vec4 mesh_rot;
};

And know how to pass a storage buffer as an array of structs by shoving them into a vector and do memcpy on vector.data() with copy length as sizeof(GPUStorage) * vector.size().

But I haven't found anywhere how does the C++ syntax look for a struct containing a variable length array ?

struct GPUMesh {
    glm::vec3 mesh_pos;
    glm::vec4 mesh_rot;
};

struct GPUStorage {
    ???  // variable length array of GPUMesh 
};

Upvotes: 1

Views: 1091

Answers (1)

Nicol Bolas
Nicol Bolas

Reputation: 474086

You have tricked yourself into thinking of things in a very limited way. Namely, that the only way to use a resource buffer (UBO/SSBO) from C++ is to define a C++ object type whose layout matches that of the resource. It's not.

The layout of a buffer-backed interface block defines how GLSL will interpret the bytes of data provided by the buffer. How those bytes get in those positions is entirely up to you.

A storage block defined as:

layout(binding = 0, std430) buffer Data
{
  vec2 first;
  vec4 array[];
};

The layout of this data is such that the first 8 bytes represent two floating-point values. Following this is 8 bytes that are skipped, followed by some multiple of 16 bytes with each 16-byte datum representing 4 floating-point values.

How you create that in the buffer object is up to you. You could do this:

std::size_t numArrayEntries = <get number of array entries>;
glNamedBufferStorage(buff, 16 + (16 * numArrayEntries), nullptr, GL_DYNAMIC_STORAGE_BIT);

glm::vec2 first = <get first>;
glNamedBufferSubData(buff, 0, 8, glm::value_ptr(first));

for(std::size_t entry = 0; entry < numArrayEntries; ++entry)
{
  glm::vec4 entryValue = <get actual entry value>;
  glNamedBufferSubData(buff, 16, 16, glm::value_ptr(entryValue));
}

That's obviously heavy-handed and involving a lot of uploading, but it is serviceable. You could also get the data there via mapping the buffer:

std::size_t numArrayEntries = <get number of array entries>;
std::size_t buffSize = 16 + (16 * numArrayEntries)
glNamedBufferStorage(buff, buffSize, nullptr, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);

unsigned char *data = (unsigned char*)glMapNamedBufferRange(buff, 0, buffSize, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT);

glm::vec2 first = <get first>;
memcpy(data, glm::value_ptr(first), 8);

for(std::size_t entry = 0; entry < numArrayEntries; ++entry)
{
  data += 16
  glm::vec4 entryValue = <get actual entry value>;
  memcpy(data, glm::value_ptr(entryValue));
}

Or by building a temporary memory buffer first and doing the copy when you create the storage. Or any other technique.

Upvotes: 4

Related Questions