Show Menu

Utah_teapot_simple_2

Preface

This article is intended to provide a high level view of Clairvoyance’s implementation of buffer objects and explain the decisions behind their use.

The term Buffer Object describes an allocated block of raw, linear memory. The general premise behind a buffer object is that its memory can be used in a wide variety of arbitrary ways. There is no format associated with a buffer object itself, and specializations are interpreted by the methods that use it. They can be used to store vertex data, pixel data, and other things.

Clairvoyance

Buffer objects play a crucial role in composing the fundamental structure of the Clairvoyance graphics engine. They provide a valuable level of flexibility, and enable different memory related problems to be solved from the perspective of a generic starting point. Buffer objects offer a higher degree of control and management over regular memory allocation. By designing specific implementations, we can explicitly define the ‘where’ and the ‘how’ of memory allocation to suit a certain need, such as optimizing runtime efficiency.

Clairvoyance defines a buffer object abstractly, and expects specific implementation to be done in its child classes.  The Buffer Object interface, therefore, includes methods that must necessarily exist independently of the subclass. These core functions allow the buffer to be locked and unlocked, allowing data to be loaded in or relinquished.

 

Clairvoyance's BufferObject Interface

Clairvoyance’s BufferObject Interface

Data abstraction allows handling the Buffer Object class structures in meaningful ways. Since all objects will derive from the same basic interface, they can be effectively handled by a single manager class. The manager pattern is used here to generalize buffer object operations, and takes on further responsibilities as more buffer object subclasses are defined. The Buffer Object Manager deals with the creation of Buffer Objects and provides hints to their subclasses on how to allocate their memory.

Clairvoyance's BufferObjectManager Interface

Clairvoyance’s BufferObjectManager Interface

The manager class receives calls to create and return a reference to a buffer object. Since the Buffer Object class exists only in a virtual context, so the Buffer Object Manager can only act as a base. A request to create a buffer returns a BufferObjectPtr, a Buffer Object wrapper that allows it to be shared between parent classes and simultaneously counts its references.

Vertex Buffers and Index Buffers

Two important subclasses of the Buffer Object are the Vertex Buffer Object (VBO) and the Index Buffer Object (IBO). These specialized buffers are used specifically to handle geometric data and optimize rendering operations. The decision to use VBOs and IBOs was a made based on favourable performance implications and modern rendering paradigms. This is in line with our goal to make Clairvoyance as robust and efficient as possible. Another important buffer related to rendering is the Uniform Buffer Object (UBO), which deals with data storage in a shader program.  UBOs are not within the context of this article and will be discussed in subsequent follow-ups.

Rendering APIs provide two approaches to drawing geometry. They are known traditionally as immediate mode and intermediate mode.

When using immediate mode, applications send the geometric data to the graphics processor every frame. Individual mesh elements, such as a position or normal, accumulate to form high volumes of traffic to and from system memory. This leads to a high number of CPU cycles to perform the transfer. Function call overhead from the API compounds these effects and creates further system traffic, resulting in more CPU cycles.

The alternative is intermediate mode, where the geometry is uploaded to the video device for non-immediate rendering. The data resides in the video device memory rather than the system memory, and can be rendered directly by the video device. VBOs and IBOs efficiently enhance data transfer by storing and grouping information, and drastically reduce overhead.

Implementing intermediate mode into our Buffer Object Manager yielded the following additions:

VertexBufferObjectPtr createVertexBuffer(size_t vertexSize, size_t numVertices);
IndexBufferObjectPtr createIndexBuffer(IndexType type, size_t numIndices);
UniformBufferObjectPtr createUniformBuffer(size_t uniformSize, size_t numUniforms);

Vertex Buffer Objects and Index Buffer Objects are both used in the composition of a renderable mesh. The geometrical data is collected from a 3D file format and serialized into usable information for the renderer, creating a list of vertices. A vertex structure is defined as the collection of 8 floating-point numbers (for a total of 32 bits) reserved for the x, y, and z ordinate vector, the normal vector, and the UV coordinates.  A buffer object takes in this list at the time the model is loaded into memory, and is then responsible for transferring the data to the GPU (via a graphics API specific subclass).

The following demonstrates the implementation of writing to the buffer, used in both vertex and index buffer objects:

void* lock(size_t offset, size_t size)
{
       void* buffer = BufferObjectManager::getInstance()->allocateBuffer(size);
       mBufferOffset = offset;
       mBufferSize = size;
       mBufferPtr = buffer;
       read(offset, size, buffer);
       return buffer;
}

Index buffer objects take advantage of shared vertices, and provide the renderer with a list of indices that map to a given vertex. IBOs and VBOs work in tandem with each other, and share a lot of common functionality, such as the implementations of their lock and unlock methods. An important optimization we made in defining the Index Buffer Object class is to dynamically determine the appropriate bit size of its indices (either 16 bit or 32 bit) to minimize overhead to the buffer.

By writing to the buffer at the time the model is loaded, we are enabling ourselves to use a high level of polygons that would be otherwise impossible to implement in immediate mode.

OpenGL

While writing Clairvoyance, we strive to keep the implementation of a specific graphics API separate from the lower levels of abstraction. OpenGL is currently being implemented through a series of subclasses that build upon a hierarchy of granular components. The OpenGL API provides the specific methods needed to make VBO and IBO transfer possible.

Clairvoyance's Implementation of GL Buffering

Clairvoyance’s Implementation of GL Buffering

Arranging the OpenGL methods into the existing buffer object interface proved fairly simple, and required only adding in a few specific functions. The constructor generates and binds the buffer through the calls:

glGenBuffers(1, &mBufferID);
glBindBuffer(target, mBufferID);

where target is one of either enumerations GL_ARRAY_BUFFER_ARB or GL_ELEMENT_ARRAY_BUFFER_ARB. Similarly, the destructor makes the call:

glDeleteBuffers(1, &mBufferID);

Reading and writing methods compose of glGetBufferSubData and glBufferSubData respectively, while the lock and unlock implementations remain exactly the same.

The OpenGL render system in Clairvoyance looks at the data stored by the buffers and selects the appropriate BufferObjectPtr depending on whichever model it is currently rendering.  These buffers contain all the information needed to fill the parameters for the intermediate rendering methods. At the core of the our render method, we have the functions glVertexPointer(..), glNormalPointer(..), and glTexCoordPointer(..). Since we previously defined our vertex structure to have a 32-bit alignment and store a position, normal, and texture coordinate, we have all the pertinent information necessary to fill in the parameters of the rendering functions.

Utah teapot simple 2” by Dhatfield. Licensed under CC BY-SA 3.0 via Wikimedia Commons

Clairvoyance Architecture: Buffer Objects

The term Buffer Object describes an allocated block of raw, linear memory. The general premise behind a buffer object is that its memory can be used in a wide variety of arbitrary ways. There is no format associated with a buffer object itself, and specializations are interpreted by the methods that use it. They can be used to store vertex data, pixel data, and other things.