Home > Articles > Programming > Graphic Programming

  • Print
  • + Share This
This chapter is from the book

Data in OpenGL Buffers

Almost everything you will ever do with OpenGL will involve buffers full of data. Buffers in OpenGL are represented as buffer objects. You’ve already had a brief introduction to buffer objects in Chapter 1. However, in this section we’ll dig a little deeper into the specifics of how buffer objects are used; ways to create, manage, and destroy them; and the best practices associated with buffer objects.

Creating and Allocating Buffers

As with many things in OpenGL, buffer objects are named using GLuint values. Values are reserved using the glGenBuffers() command. This function has already been described in Chapter 1, but we include the prototype here again for handy reference.

After calling glGenBuffers(), you will have an array of buffer object names in buffers, but at this time, they’re just placeholders. They’re not actually buffer objects—yet. The buffer objects themselves are not actually created until the name is first bound to one of the buffer binding points on the context. This is important because OpenGL may make decisions about the best way to allocate memory for the buffer object based on where it is bound. The buffer binding points (called targets) are described in Table 3.2.

Table 3.2. Buffer Binding Targets

Target

Uses

GL_ARRAY_BUFFER

This is the binding point that is used to set vertex array data pointers using glVertexAttribPointer(). This is the target that you will likely use most often.

GL_COPY_READ_BUFFER and GL_COPY_WRITE_BUFFER

Together, these targets form a pair of binding points that can be used to copy data between buffers without disturbing OpenGL state, or implying usage of any particular kind to OpenGL.

GL_DRAW_INDIRECT_BUFFER

A buffer target used to store the parameters for drawing commands when using indirect drawing, which will be explained in detail in the next section.

GL_ELEMENT_ARRAY_BUFFER

Buffers bound to this target can contain vertex indices which are used by indexed draw commands such as glDrawElements().

GL_PIXEL_PACK_BUFFER

The pixel pack buffer is used as the destination for OpenGL commands that read data from image objects such as textures or the framebuffer.Examples of such commands include glGetTexImage() and glReadPixels().

GL_PIXEL_UNPACK_BUFFER

The pixel unpack buffer is the opposite of the pixel pack buffer---it is used as the source of data for commands like glTexImage2D().

GL_TEXTURE_BUFFER

Texture buffers are buffers that are bound to texture objects so that their data can be directly read inside shaders. The GL_TEXTURE_BUFFER binding point provides a target for manipulating these buffers, although they must still be attached to textures to make them accessible to shaders.

GL_TRANSFORM_FEEDBACK_ BUFFER

Transform feedback is a facility in OpenGL whereby transformed vertices can be captured as they exit the vertex processing part of the pipeline (after the vertex or geometry shader, if present) and some of their attributes written into buffer objects. This target provides a binding pointfor buffers that are used to record those attributes. Transform feedback will be covered in some detail in“Transform Feedback” on Page 239.

GL_UNIFORM_BUFFER

This target provides a binding point where buffers that will be used as uniform buffer objects may be bound. Uniform buffers are covered in Subsection 2, “Uniform Blocks”.

A buffer object actually is created by binding one of the names reserved by a call to glGenBuffers() to one of the targets in Table 3.2 using glBindBuffer(). As with glGenBuffers(), glBindBuffer() was introduced in Chapter 1, but we include its prototype here again for completeness.

Right, so we now have a buffer object bound to one of the targets listed in Table 3.2, now what? The default state of a newly created buffer object is a buffer with no data in it. Before it can be used productively, we must put some data into it.

Getting Data into and out of Buffers

There are many ways to get data into and out of buffers in OpenGL. These range from explicitly providing the data, to replacing parts of the data in a buffer object with new data, to generating the data with OpenGL and recording it into the buffer object. The simplest way to get data into a buffer object is to load data into the buffer at time of allocation. This is accomplished through the use of the glBufferData() function. Here’s the prototype of glBufferData() again.

It’s important to note that glBufferData() actually allocates (or reallocates) storage for the buffer object. That is, if the size of the new data is greater than the current storage space allocated for the buffer object, the buffer object will be resized to make room. Likewise, if the new data is smaller than what has been allocated for the buffer, the buffer object will shrink to match the new size. The fact that it is possible to specify the initial data to be placed into the buffer object is merely a convenience and is not necessarily the best way to do it (or even the most convenient, for that matter).

The target of the initial binding is not the only information OpenGL uses to decide how to best allocate the buffer object’s data store. The other important parameter to glBufferData() is the usage parameter. usage must be one of the standard usage tokens such as GL_STATIC_DRAW or GL_DYNAMIC_COPY. Notice how the token name is made of two parts—the first being one of STATIC, DYNAMIC, or STREAM and the second being one of DRAW, READ, or COPY.

The meanings of these “subtokens” are shown in Table 3.3.

Table 3.3. Buffer Usage Tokens

Token Fragment

Meaning

_STATIC_

The data store contents will be modified once and used many times.

_DYNAMIC_

The data store contents will be modified repeatedly and used many times.

_STREAM_

The data store contents will be modified once and used at most a few times.

_DRAW

The data store contents are modified by the application and used as the source for OpenGL drawing and image specification commands.

_READ

The data store contents are modified by reading data from OpenGL and used to return that data when queried by the application.

_COPY

The data store contents are modified by reading data from OpenGL and used as the source for OpenGL drawing and image specification commands.

Accurate specification of the usage parameter is important to achieve optimal performance. This parameter conveys useful information to OpenGL about how you plan to use the buffer. Consider the first part of the accepted tokens first. When the token starts with _STATIC_, this indicates that the data will change very rarely, if at all—it is essentially static. This should be used for data that will be specified once and never modified again. When usage includes _STATIC_, OpenGL may decide to shuffle the data around internally in order to make it fit in memory better, or be a more optimal data format. This may be an expensive operation, but since the data is static, it needs to be performed only once and so the payoff may be great.

Including _DYNAMIC_ in usage indicates that you’re going to change the data from time to time but will probably use it many times between modifications. You might use this, for example, in a modeling program where the data is essentially static—until the user edits it. In this case, it’ll probably be used for many frames, then be modified, and then used for many more frames, and so on. This is in contrast to the GL_STREAM_ subtoken. This indicates that you’re planning on regularly modifying the data in the buffer and using it only a few times (maybe only once) between each modification. In this case, OpenGL might not even copy your data to fast graphics memory if it can access it in place. This should be used for applications such as physical simulations running on the CPU where a new set of data is presented in each frame.

Now turn your attention to the second part of the usage tokens. This part of the token indicates who is responsible for updating and using the data. When the token includes _DRAW, this infers that the buffer will be used as a source of data during regular OpenGL drawing operations. It will be read a lot, compared to data whose usage token includes _READ, which is likely to be written often. Including _READ indicates that the application will read back from the buffer (see “Accessing the Content of Buffers”), which in turn infers that the data is likely to be written to often by OpenGL. usage parameters including _DRAW should be used for buffers containing vertex data, for example, whereas parameters including _READ should be used for pixel buffer objects and other buffers that will be used to retrieve information from OpenGL. Finally, including _COPY in usage indicates that the application will use OpenGL to generate data to be placed in the buffer, which will then be used as a source for subsequent drawing operations. An example of an appropriate use of _COPY is transform feedback buffers—buffers that will be written by OpenGL and then be used as vertex buffers in later drawing commands.

Initializing Part of a Buffer

Suppose you have an array containing some vertex data, another containing some color information, and yet another containing texture coordinates or some other data. You’d like to pack the data back to back into one big buffer object so that OpenGL can use it. The arrays may or may not be contiguous in memory, so you can’t use glBufferData() to upload all of it in one go. Further, if you use glBufferData() to upload, say, the vertex data first, then the buffer will be sized to exactly match the vertex data and there won’t be room for the color or texture coordinate information. That’s where glBufferSubData() comes in.

By using a combination of glBufferData() and glBufferSubData(), we can allocate and initialize a buffer object and upload data into several separate sections of it. An example is shown in Example 3.1.

Example 3.1. Initializing a Buffer Object with glBufferSubData()

// Vertex positions
static const GLfloat positions[] =
{
      -1.0f, -1.0f, 0.0f, 1.0f,
       1.0f, -1.0f, 0.0f, 1.0f,
       1.0f,  1.0f, 0.0f, 1.0f,
      -1.0f,  1.0f, 0.0f, 1.0f
};



// Vertex colors
static const GLfloat colors[] =
{
      1.0f, 0.0f, 0.0f,
      0.0f, 1.0f, 0.0f,
      0.0f, 0.0f, 1.0f,
      1.0f, 1.0f, 1.0f,
};



// The buffer object
GLuint buffer;



// Reserve a name for the buffer object.
glGenBuffers(1, &buffer);
// Bind it to the GL_ARRAY_BUFFER target.
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// Allocate space for it (sizeof(positions) + sizeof(colors)).
glBufferData(GL_ARRAY_BUFFER,                       // target
             sizeof(positions) + sizeof(colors),    // total size
             NULL,                                  // no data
             GL_STATIC_DRAW);                       // usage
// Put "positions" at offset zero in the buffer.
glBufferSubData(GL_ARRAY_BUFFER,                    // target
                0,                                  // offset
                sizeof(positions),                  // size
                positions);                         // data
// Put "colors" at an offset in the buffer equal to the filled size of
// the buffer so far - i.e., sizeof(positions).
glBufferSubData(GL_ARRAY_BUFFER,                    // target
                sizeof(positions),                  // offset
                sizeof(colors),                     // size
                colors);                            // data
// Now "positions" is at offset 0 and "colors" is directly after it
// in the same buffer.

If you simply wish to clear a buffer object’s data store to a known value, you can use the glClearBufferData() or glClearBufferSubData() functions. Their prototypes are as follows:

Using glClearBufferData() or glClearBufferSubData() allows you to initialize the data store of a buffer object without necessarily reserving and clearing a region of system memory to do it.

Data can also be copied between buffer objects using the glCopyBufferSubData() function. Rather than assembling chunks of data in one large buffer object using glBufferSubData(), it is possible to upload the data into separate buffers using glBufferData() and then copy from those buffers into the larger buffer using glCopyBufferSubData(). Depending on the OpenGL implementation, it may be able to overlap these copies because each time you call glBufferData() on a buffer object, it invalidates whatever contents may have been there before. Therefore, OpenGL can sometimes just allocate a whole new data store for your data, even though a copy operation from the previous store has not completed yet. It will then release the old storage at a later opportunity.

The prototype of glCopyBufferSubData() is as follows:

Whilst glCopyBufferSubData() can be used to copy data between buffers bound to any two targets, the targets GL_COPY_READ_BUFFER and GL_COPY_WRITE_BUFFER are provided specifically for this purpose. Neither target is used for anything else by OpenGL, and so you can safely bind buffers to them for the purposes of copying or staging data without disturbing OpenGL state or needing to keep track of what was bound to the target before your copy.

Reading the Contents of a Buffer

Data can be read back from a buffer object in a couple of different ways. The first is to use the glGetBufferSubData() function. This function reads data from the buffer object bound to one of the targets and places it into a chunk of memory owned by your applications. The prototype of glGetBufferSubData() is as follows:

glGetBufferSubData() is useful when you have generated data using OpenGL and wish to retrieve it. Examples include using transform feedback to process vertices using a GPU, or reading framebuffer or texture data into a Pixel Buffer Object. Both of these topics will be covered later. Of course, it’s also possible to use glGetBufferSubData() to simply read back data that you previously put into the buffer object.

Accessing the Content of Buffers

The issue with all of the functions covered in this section so far (glBufferData(), glBufferSubData(), glCopyBufferSubData(), and glGetBufferSubData()) is that they all cause OpenGL to make a copy of your data. glBufferData() and glBufferSubData() both copy data from your application’s memory into memory owned by OpenGL. Obviously, glCopyBufferSubData() causes a copy of previously buffered data to be made. glGetBufferSubData() copies data from memory owned by OpenGL into memory provided by your application. Depending on the hardware configuration, it’s very possible that the memory owned by OpenGL would be accessible to your application if only you had a pointer to it. Well, you can get that pointer using glMapBuffer().

When you call glMapBuffer(), the function returns a pointer to memory that represents the data store of the buffer object attached to target. Note that this memory represents only this buffer—it is not necessarily the memory that the graphics processor will use. The access parameter specifies how the application intends to use the memory once it is mapped. It must be one of the tokens shown in Table 3.4.

Table 3.4. Access Modes for glMapBuffer()

Token

Meaning

GL_READ_ONLY

The application will only read from the memory mapped by OpenGL.

GL_WRITE_ONLY

The application will only write to the memory mapped by OpenGL.

GL_READ_WRITE

The application may read from or write to the memory mapped by OpenGL.

If glMapBuffer() fails to map the buffer object’s data store, it returns NULL. The access parameter forms a contract between you and OpenGL that specifies how you will access the memory. If you violate that contract, bad things will happen, which may include ignoring writes to the buffer, corrupting your data or even crashing your program.3

When the buffer is mapped with the GL_READ_ONLY or GL_READ_WRITE access mode, the data that was in the buffer object becomes visible to your application. You can read it back, write it to a file, and even modify it in place (so long as you used GL_READ_WRITE as the access mode). If access is GL_READ_WRITE or GL_WRITE_ONLY, you can write data into memory using the pointer OpenGL gave you. Once you are done using the data or writing data into the buffer object, you must unmap it using glUnmapBuffer(), whose prototype is as follows:

When you unmap the buffer, any data you wrote into the memory given to you by OpenGL becomes visible in the buffer object. This means that you can place data into buffer objects by allocating space for them using glBufferData() and passing NULL as the data parameter, mapping them, writing data into them directly, and then unmapping them again. Example 3.2 contains an example of loading the contents of a file into a buffer object.

Example 3.2. Initializing a Buffer Object with glMapBuffer()

GLuint buffer;
FILE * f;
size_t filesize;

// Open a file and find its size
f = fopen("data.dat", "rb");
fseek(f, 0, SEEK_END);
filesize = ftell(f);
fseek(f, 0, SEEK_SET);

// Create a buffer by generating a name and binding it to a buffer
// binding point - GL_COPY_WRITE_BUFFER here (because the binding means
// nothing in this example).
glGenBuffers(1, &buffer);
glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);

// Allocate the data store for the buffer by passing NULL for the
// data parameter.
glBufferData(GL_COPY_WRITE_BUFFER, (GLsizei)filesize, NULL,
             GL_STATIC_DRAW);
// Map the buffer...
void * data = glMapBuffer(GL_COPY_WRITE_BUFFER, GL_WRITE_ONLY);

// Read the file into the buffer.
fread(data, 1, filesize, f);

// Okay, done, unmap the buffer and close the file.
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
fclose(f);

In Example 3.2, the entire contents of a file are read into a buffer object in a single operation. The buffer object is created and allocated to the same size as the file. Once the buffer is mapped, the file can be read directly into the buffer object’s data store. No copies are made by the application, and, if the data store is visible to both the application and the graphics processor, no copies will be made by OpenGL.

There may be significant performance advantages to initializing buffer objects in this manner. The logic is this; when you call glBufferData() or glBufferSubData(), once those functions return, you are free to do whatever you want with the memory you gave them—free it, use it for something else—it doesn’t matter. This means that those functions must be done with that memory by the time they return, and so they need to make a copy of your data. However, when you call glMapBuffer(), the pointer you get points at memory owned by OpenGL. When you call glUnmapBuffer(), OpenGL still owns that memory—it’s the application that has to be done with it. This means that if the data needs to be moved or copied, OpenGL can start that process when you call glUnmapBuffer() and return immediately, content in the knowledge that it can finish the operation at its leisure without your application interfering in any way. Thus the copy that OpenGL needs to perform can overlap whatever your application does next (making more buffers, reading more files, and so on). If it doesn’t need to make a copy, then great! The unmap operation essentially becomes free in that case.

Asynchronous and Explicit Mapping

To address many of the issues involved with mapping buffers using glMapBuffer() (such as applications incorrectly specifying the access parameter or always using GL_READ_WRITE), glMapBufferRange() uses flags to specify access more precisely. The prototype for glMapBufferRange() is as follows:

For glMapBufferRange(), access is a bitfield that must contain one or both of the GL_MAP_READ_BIT and the GL_MAP_WRITE_BIT indicating whether the application plans to read from the mapped data store, write to it, or do both. In addition, access may contain one or more of the flags shown in Table 3.5.

Table 3.5. Flags for Use with glMapBufferRange()

Flag

Meaning

GL_MAP_INVALIDATE_RANGE_BIT

If specified, any data in the specified range of the buffer may be discarded and considered invalid. Any data within the specified range that is not subsequently written by the application becomes undefined. This flag may not be used with GL_MAP_READ_BIT.

GL_MAP_INVALIDATE_BUFFER_BIT

If specified, the entire contents of the buffer may be discarded and considered invalid, regardless of the specified range.Any data lying outside the mapped range of the buffer object becomes undefined,as does any data within the range but not subsequently written by the application.This flag may not be used with GL_MAP_READ_BIT.

GL_MAP_FLUSH_EXPLICIT_BIT

The application will take responsibility to signal to OpenGL which parts of the mapped range contain valid data by calling glFlushMappedBufferRange() prior to calling glUnmapBuffer(). Use this flag if a larger range of the buffer will be mapped and not all of it will be written by the application. This bit must be used in conjunction with GL_MAP_WRITE_BIT. If GL_MAP_FLUSH_EXPLICIT_BIT is not specified, glUnmapBuffer() will automatically flush the entirety of the mapped range.

GL_MAP_UNSYNCHRONIZED_BIT

If this bit is not specified, OpenGL will wait until all pending operations that may access the buffer have completed before returning the mapped range. If this flag is set, OpenGL will not attempt to synchronize operations on the buffer.

As you can see from the flags listed in Table 3.5, the command provides a significant level of control over how OpenGL uses the data in the buffer and how it synchronizes operations that may access that data.

When you specify that you want to invalidate the data in the buffer object by specifying either the GL_MAP_INVALIDATE_RANGE_BIT or GL_MAP_INVALIDATE_BUFFER_BIT, this indicates to OpenGL that it is free to dispose of any previously stored data in the buffer object. Either of the flags can be set only if you also specify that you’re going to write to the buffer by also setting the GL_MAP_WRITE_BIT flag. If you specify GL_MAP_INVALIDATE_RANGE_BIT, it indicates that you will update the entire range (or at least all the parts of it that you care about). If you set the GL_MAP_INVALIDATE_BUFFER_BIT, it means that you don’t care what ends up in the parts of the buffer that you didn’t map. Either way, setting the flags indicates that you’re planning to update the rest of the buffer with subsequent maps.4 When OpenGL is allowed to throw away the rest of the buffer’s data, it doesn’t have to make any effort to merge your modified data back into the rest of the original buffer. It’s probably a good idea to use GL_MAP_INVALIDATE_BUFFER_BIT for the first section of the buffer that you map, and then GL_MAP_INVALIDATE_RANGE_BIT for the rest of the buffer.

The GL_MAP_UNSYNCHRONIZED_BIT flag is used to disengage OpenGL’s automatic synchronization between data transfer and use. Without this bit, OpenGL will finish up any in-flight commands that might be using the buffer object. This can stall the OpenGL pipeline, causing a bubble and a loss of performance. If you can guarantee that all pending commands will be complete before you actually modify the contents of the buffer (but not necessarily before you call glMapBufferRange()) through a method such as calling glFinish() or using a sync object (which are described in “Atomic Operations and Synchronization” on Page 578 in Chapter 11), then OpenGL doesn’t need to do this synchronization for you.

Finally, the GL_MAP_FLUSH_EXPLICIT_BIT flag indicates that the application will take on the responsibility of letting OpenGL know which parts of the buffer it has modified before calling glUnmapBuffer(). It does this through a call to glFlushMappedBufferRange(), whose prototype is as follows:

It is possible to call glFlushMappedBufferRange() multiple times on separate or even overlapping ranges of a mapped buffer object. The range of the buffer object specified by offset and length must lie within the range of buffer object that has been mapped, and that range must have been mapped by a call to glMapBufferRange() with access including the GL_MAP_FLUSH_EXPLICIT_BIT flag set. When this call is made, OpenGL assumes that you’re done modifying the specified range of the mapped buffer object, and can begin any operations it needs to perform in order to make that data usable such as copying it to graphics processor visible memory, or flushing, or invalidating data caches. It can do these things even though some or all of the buffer is still mapped. This is a useful way to parallelize OpenGL with other operations that your application might perform. For example, if you need to load a very large piece of data from a file into a buffer, map a range of the buffer large enough to hold the whole file, then read chunks of the file, and after each chunk call glFlushMappedBufferRange(). OpenGL will then operate in parallel to your application, reading more data from the file for the next chunk.

By combining these flags in various ways, it is possible to optimize data transfer between the application and OpenGL or to use advanced techniques such as multithreading or asynchronous file operations.

Discarding Buffer Data

Advanced

When you are done with the data in a buffer, it can be advantageous to tell OpenGL that you don’t plan to use it any more. For example, consider the case where you write data into a buffer using transform feedback, and then draw using that data. If that drawing command is the last one that is going to access the data, then you can tell OpenGL that it is free to discard the data and use the memory for something else. This allows an OpenGL implementation to make optimizations such as tightly packing memory allocations or avoiding expensive copies in systems with more than one GPU.

To discard some or all of the data in a buffer object, you can call glInvalidateBufferData() or glInvalidateBufferSubData(), respectively. The prototypes of these functions are as follows:

Note that semantically, calling glBufferData() with a NULL pointer does a very similar thing to calling glInvalidateBufferData(). Both methods will tell the OpenGL implementation that it is safe to discard the data in the buffer. However, glBufferData() logically recreates the underlying memory allocation, whereas glInvalidateBufferData() does not. Depending on the OpenGL implementation, it may be more optimal to call glInvalidateBufferData(). Further, glInvalidateBufferSubData() is really the only way to discard a region of a buffer object’s data store.

  • + Share This
  • 🔖 Save To Your Account