I solved it. Turns out, the code above was fine, and the issue was within my vertex buffer:
glGenBuffers(2, vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * numVertices, vertices, GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint16_t) * numIndices, indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)sizeof(Vector2)); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); delete vertices; delete indices;
My second vertex attribute was set to 4 for the size, GL_UNSIGNED_BYTE for the type and GL_TRUE for the normalized arguments. This was a simple copy and paste mistake as the vertex format from another test state uses position and color. This one uses position and texture coordinates. False alarm!