Back

PixelBoy - Rendering Commands

Game Engine c++ Rendering OpenGL SDL PixelBoy Graphics Programming Architecture

For my 2D pixel art game engine in C/C++ I needed a way to separate the rendering from the game logic. On the project page for 'Pixelboy Game' I've talked about how I wanted to achieve a way to have the game logic separate from the platform layer. I wanted to do the same for the graphics API.

Eventually I might want to change from OpenGL (what I'm currently using) to Vulcan, or support multiple APIs for different platforms (DirectX 12 for Xbox, PlayStation's proprietary API or Metal on MacOS). This requires that both the platform layer and the game have a platform agnostic way of talking to the abstract graphics API. Both Casy Muratori and Unity use a very easy to grasp model to achieve this: command buffers.

A command buffer is a buffer of differently sized (in bytes) graphics commands that are send to the Graphics API. For example, draw a sprite here, a line there and render some command buffer to this target render texture. Most if not all current Graphics APIs support these kind of commands, but implement the way these commands are handled differently. APIs like Vulcan themselves work in a commands-based way, while OpenGL is much more immediate and state based.

By adopting this model I hope to generalize the commands from the perspective of the game while keeping the API implementation hidden from the game and platform. The platform should just be able to say: initialize graphics API and the game should be able to say: render all these things in this order (or the order should not matter). Plus, I've always liked the command buffer approach in Unity for its flexibility.

PushBuffers

This approach requires some kind of polymorphic array or list. However, I had no idea of how to achieve this in c. I looked at Casey Muratori's Handmade Hero for inspiration, and what I found is quite clever. Het introduced me to the concept of a push buffer. A pre-allocated size where you push differently sized data structures and just keep a pointer to the start and the size currently filled (or a pointer to the next available address).

#include <malloc.h>
#include <cassert>

typedef struct PushBuffer {
    unsigned char* data;
    size_t size_used;
    size_t total_size;
} PushBuffer;

PushBuffer AllocatePushBuffer(size_t size) {
    PushBuffer result = {};
    result.data = (unsigned char*)malloc(size);
    result.size_used = 0;
    result.total_size = size;
    return result;
}

unsigned char* PushSize(PushBuffer push_buffer, size_t size) {
    assert(push_buffer.size_used + size < push_buffer.total_size);
    push_buffer.size_used += size;
    return push_buffer.data + push_buffer.size_used;
}

#define PushStruct(push_buffer, Type) (Type*)PushSize(push_buffer, sizeof(Type));

/////////// Example ///////////

typedef struct A {
 int i;
};
 
int main(char* input, char** var_args) {
 PushBuffer push_buffer = AllocatePushBuffer(1024);
 A* a = PushStruct(push_buffer, A);
 a->i = 10;
}

C-style Polymorphism

This solves the problem of having to store multi-size structures into a single array. The next problem is, how do we iterate over the array and know how far we need to advance the pointer into the buffer and how do we know what we should cast the pointer to? The solution that I found works fine is to have all structures (graphics commands in this case) all have the same header, where the header tells what type of structure follows the header. If we know that, we can simply cast the pointer to the correct type and advance the pointer with the size of the struct.

#include "PushBuffer.hpp";

enum GraphicsCommandTypes {
 GraphicsCommandType_Sprite,
 GraphicsCommandType_DebugRect,
 GraphicsCommandType_DebugLine,
};

typedef struct GraphicsCommandHeader {
 GraphicsCommandTypes type;
} GraphicsCommandHeader;

typedef struct SpriteCommand {
 GraphicsCommandHeader header;
 V2 center;
 V2 size;
 Texture* spriteSheet;
 Rect sprite;
 Color overlayColor;
} SpriteCommand;

typedef struct DebugRectCommand {
 GraphicsCommandHeader header;
 V2 center;
 V2 size;
 Color color;
} DebugRectCommand;

typedef struct DebugLineCommand {
 GraphicsCommandHeader header;
 V2 point1;
 V2 point2;
} DebugLineCommand;
#include "GraphicsCommands.cpp"
#include "PushBuffer.hpp"
#include "GraphicsAPI.hpp"

void RenderCommandBuffer(GraphicsAPI* graphics_api, PushBuffer* command_buffer) {
    size_t command_pointer_offset = 0;
    for (; command_pointer_offset < command_buffer->size_used;) {
        
        GraphicsCommandHeader* header = (GraphicsCommandHeader*)(command_buffer->data + command_pointer_offset);
        command_pointer_offset += sizeof(GraphicsCommandHeader);
        
        switch (header->type) {
            case GraphicsCommandType_Sprite: {
                SpriteCommand* graphics_command = (SpriteCommand*)(command_buffer->data + command_pointer_offset);
                graphics_api->RenderSpriteCommand(graphics_command);
                command_pointer_offset += sizeof(SpriteCommand);
            } break;

            case GraphicsCommandType_DebugRect: {
                DebugRectCommand* graphics_command = (DebugRectCommand*)(command_buffer->data + command_pointer_offset);
                graphics_api->RenderDebugRectCommand(graphics_command);
                command_pointer_offset += sizeof(DebugRectCommand);
            } break;

            case GraphicsCommandType_DebugLine: {
                DebugLineCommand* graphics_command = (DebugLineCommand*)(command_buffer->data + command_pointer_offset);
                graphics_api->RenderDebugLineCommand(graphics_command);
                command_pointer_offset += sizeof(DebugLineCommand);
            } break;

						default: {
								assert(false);
						} break;
        }
    }
}

Conclusion

This concludes the blog post on the rendering commands. Below is an example of how one of these render commands is used in the game for the OpenGL renderer. It implements the function for drawing a sprite from a sprite-sheet through OpenGL.

static void DrawSprite(Camera camera, GLuint shader_program, SpriteCommandData* sprite_command) {
    float one_over_texture_width = 1.0f  sprite_command->sprite.texture->width;
    float one_over_texture_height = 1.0f  sprite_command->sprite.texture->height;
    
    // Bind buffers to shader.
    Rect rect = sprite_command->sprite.rect;
    
    float minx = -sprite_command->origin.x + 0;
    float miny = -sprite_command->origin.y + 0;
    float maxx = -sprite_command->origin.x + rect.width * PIXELS_TO_METERS;
    float maxy = -sprite_command->origin.y + rect.height * PIXELS_TO_METERS;
    
    bool hflip = (sprite_command->sprite.flip  SDL_FLIP_HORIZONTAL) > 0;
    bool vflip = (sprite_command->sprite.flip  SDL_FLIP_VERTICAL) > 0;
    
    float uv_x1 = rect.x * one_over_texture_width;
    float uv_y1 = 1.0f - ((rect.y + rect.height) * one_over_texture_height);
    float uv_x2 = (rect.x + rect.width) * one_over_texture_width;
    float uv_y2 = 1.0f - (rect.y * one_over_texture_height);
    
    float uv_minx = hflip  uv_x2 : uv_x1;
    float uv_miny = vflip  uv_y2 : uv_y1;
    float uv_maxx = hflip  uv_x1 : uv_x2;
    float uv_maxy = vflip  uv_y1 : uv_y2;
    
    glm::vec3 color0(1.0f, 1.0f, 1.0f);
    glm::vec3 color1(1.0f, 1.0f, 1.0f);
    glm::vec3 color2(1.0f, 1.0f, 1.0f);
    glm::vec3 color3(1.0f, 1.0f, 1.0f);
    // NOTE: 0.5f since the tile is center-scale oriented.
    float vertices[] = {
        // positions      // colors                     // texture coords
        minx, miny, 0.0f, color0.r, color0.g, color0.b, uv_minx, uv_miny, // top right
        maxx, miny, 0.0f, color1.r, color1.g, color1.b, uv_maxx, uv_miny, // bottom right
        maxx, maxy, 0.0f, color2.r, color2.g, color2.b, uv_maxx, uv_maxy, // bottom left
        minx, maxy, 0.0f, color3.r, color3.g, color3.b, uv_minx, uv_maxy  // top left
    };
    unsigned int indices[] = {
        0, 1, 2, // First triangle
        0, 2, 3  // Second triangle
    };
    
    unsigned int vertex_buffer_object, vertex_array_object, entities_buffer_object;
    glGenVertexArrays(1, vertex_array_object);
    glGenBuffers(1, vertex_buffer_object);
    glGenBuffers(1, entities_buffer_object);
    // NOTE: bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(vertex_array_object);
    
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, entities_buffer_object);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    
    // NOTE: Position attribute
    int points_per_vertex = 3;
    int stride = 8 * sizeof(float);
    glVertexAttribPointer(0, points_per_vertex, GL_FLOAT, GL_FALSE, stride, (void*)(0));
    // NOTE: Enable attribute index 0 in the vertex-shader.
    int vertex_position_attribute_index = 0;
    glEnableVertexAttribArray(vertex_position_attribute_index);
    
    // NOTE: Color attribute
    glVertexAttribPointer(1, points_per_vertex, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    
    // NOTE: UV-Texture-coordinates
    points_per_vertex = 2;
    glVertexAttribPointer(2, points_per_vertex, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);
    
    glUseProgram(shader_program);
    
    float rotation_angle_x = 0.0f;
    Vector2 scale = { 1.0, 1.0 };
    SetMatrices(camera, rotation_angle_x, scale, sprite_command->worldPosition, shader_program);

    glBindTexture(GL_TEXTURE_2D, sprite_command->sprite.texture->opengl_texture_handle);
    glBindVertexArray(vertex_array_object);
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

    glBindVertexArray(0);
    
    glDeleteVertexArrays(1, vertex_array_object);
    glDeleteBuffers(1, vertex_buffer_object);
    glDeleteBuffers(1, entities_buffer_object);
}