Search code examples
c++cportabilitymemory-alignmentstructure-packing

Portable way to create heterogenous vertex data array


It is very common in graphics programming to work with vertex formats. This is described, for example, here.

However, I am looking for a way to accomplish that which does not invoke undefined behavior (I'm mainly looking for C++ info, but C would be fine, too).

The common way to do it is like this: First, declare your vertex format as a struct.

struct Vertex {
    float x;
    float y;
    uint16_t someData;
    float etc;
};

Then, you create an array of these, fill them in, and send them to your graphics API (eg: OpenGL).

Vertex myVerts[100];
myVerts[0].x = 42.0;
// etc.

// when done, send the data along:
graphicsApi_CreateVertexBuffer(&myVerts[0], ...);

(Aside: I skipped the part where you tell the API what the format is; we'll just assume it knows).

However, the graphics API has no knowledge about your struct. It just wants a sequence of values in memory, something like:

|<---  first vertex   -->||<---  second vertex  -->| ...
[float][float][u16][float][float][float][u16][float] ...

And thanks to issues of packing and alignment, there is no guarantee that myVerts will be laid out that way in memory.

Of course, tons of code is written this way, and it works, despite not being portable.

But is there any portable way to do this that is not either

1. Inefficient
2. Awkward to write

?

This is basically a serialization problem. See also: Correct, portable way to interpret buffer as a struct

The main standards-compliant way I know of is to allocate your memory as char[]. Then, you just fill in all the bytes exactly how you want them laid out.

But to transform from the struct Vertex representation above to that char[] representation would require an extra copy (and a slow byte-by-byte one, at that). So that's inefficient.

Alternatively, you could write data into the char[] representation directly, but that's extremely awkward. It's much nicer to say verts[5].x = 3.0f than addressing into a byte array, writing a float as 4 bytes, etc.

Is there a good, portable way to do this?


Solution

  • However, the graphics API has no knowledge about your struct. It just wants a sequence of values in memory, something like:

    |<---  first vertex   -->||<---  second vertex  -->| ...
    [float][float][u16][float][float][float][u16][float] ...
    

    This is not true. The graphics API has knowledge about your struct, because you told it about your struct. You even know this already:

    (Aside: I skipped the part where you tell the API what the format is; we'll just assume it knows).

    When you tell the graphics API where each field is in your struct, you should use sizeof and offsetof instead of guessing the layout of your struct. Then your code will work even if the compiler inserts padding. For example (in OpenGL):

    struct Vertex {
        float position[2];
        uint16_t someData;
        float etc;
    };
    
    glVertexAttribPointer(position_index, 2, GL_FLOAT, GL_FALSE, sizeof(struct Vertex), (void*)offsetof(struct Vertex, position));
    glVertexAttribIPointer(someData_index, 1, GL_SHORT, sizeof(struct Vertex), (void*)offsetof(struct Vertex, someData));
    glVertexAttribPointer(etc_index, 1, GL_FLOAT, GL_FALSE, sizeof(struct Vertex), (void*)offsetof(struct Vertex, etc));
    

    not

    glVertexAttribPointer(position_index, 2, GL_FLOAT, GL_FALSE, 14, (void*)0);
    glVertexAttribIPointer(someData_index, 1, GL_SHORT, 14, (void*)8);
    glVertexAttribPointer(etc_index, 1, GL_FLOAT, GL_FALSE, 14, (void*)10);
    

    Of course, if you were reading your vertices from disk as a blob of bytes in a known format (which could be different from the compiler's struct layout), then you may use a hardcoded layout to interpret those bytes. If you're treating vertices as an array of structs, then use the layout the compiler has decided for you.