Before going on I should clarify one point. It's common for writers to refer to themselves in the third person as "the author". In these blog posts regarding this book the term "the author" refers to Mr. Marucchi‑Foino. When speaking of myself in these posts I will use personal pronouns (I, me, my, mine, etc.).
Some of the changes to the PROGRAM data type are similar to changes made in the last post about the SHADER data type:
- change the declaration of the PROGRAM data type from being a typedef to a struct,
- take note of places where unsigned char variables are used to transmit boolean values and change the variables to be type bool,
- create a constructor from the PROGRAM_init() function,
- add initializer lists to the constructors to initialize data members to zero/NULL/false, as appropriate, for data members without an explicit value passed in the argument list of the constructor,
- retype various data members with appropriate OpenGL data types, and
- create a destructor from the PROGRAM_free() function.
The old PROGRAM_create() function became one of two constructors for the new PROGRAM class. Since the old PROGRAM_create() function also called the old PROGRAM_init() function to perform some initialization on its behalf, the old PROGRAM_init() function actually becomes two methods in the implementation of the new PROGRAM class: the first is a private init() method, and the second a public constructor. Both constructors call the init() method to perform some of their initialization. In future posts there will be similar examples of the old THING_init() function spawning both a private THING::init() function and a constructor.
In the constructor created from the old PROGRAM_create() the code explicitly initializes the programdrawcallback and programbindattribcallback data members; the new code initializes these data members, along with the data member pid, using an initializer list so we can remove their explicit initialization from the body of the constructor.
In the constructor created from the old PROGRAM_create() the code explicitly initializes the programdrawcallback and programbindattribcallback data members; the new code initializes these data members, along with the data member pid, using an initializer list so we can remove their explicit initialization from the body of the constructor.
The constructor created from PROGRAM_create() also allocates and initializes two SHADERs, a vertex shader and a fragment shader. The initialization steps are changed to use the new object oriented SHADER methods (the changes to the load_gfx() method parallel these changes in this constructor). Because PROGRAM data objects can have their own SHADER objects my opinion is that the destructor should, correspondingly, have code which releases the SHADERs should they exist. For this reason new code is added to the destructor which checks to see if the PROGRAM has initialized its vertex_shader and fragment_shader data members, and, if they exist, deletes them. Admittedly, there is some risk here. The programmer could separately create SHADER objects and manually set the vertex_shader and/or fragment_shader data members of a PROGRAM object to point to these separately created SHADER objects. After having done this the programmer could retain and reuse his own pointers to the SHADER objects. Deleting the SHADER objects in the PROGRAM destructor would leave these private copies of the object addresses pointing at invalid addresses. In the future we can use C++ shared_ptrs to protect us from releasing SHADER objects which are still referenced other places but in the short term I'll manage the problem manually.
The PROGRAM data type also has two arrays which it manages. In the old code each array had a count (uniform_count and vertex_attrib_count) and a pointer (uniform_array and vertex_attrib_array) to space which was dynamically allocated/reallocated to allow the array to grow, as needed. In the new implementation these two arrays are replaced with vectors from the Standard Template Library (STL). Since the code now uses vectors the PROGRAM data type no longer needs the array counts (uniform_count and vertex_attrib_count).
The PROGRAM data type also has two arrays which it manages. In the old code each array had a count (uniform_count and vertex_attrib_count) and a pointer (uniform_array and vertex_attrib_array) to space which was dynamically allocated/reallocated to allow the array to grow, as needed. In the new implementation these two arrays are replaced with vectors from the Standard Template Library (STL). Since the code now uses vectors the PROGRAM data type no longer needs the array counts (uniform_count and vertex_attrib_count).
Remember that C (and C++ by way of its C origins) allows us to syntactically interchange pointers for arrays and vice versa. Because of this while I discuss the uniform_array data member I will use the two terms, pointer and array, somewhat interchangeably. In the PROGRAM_add_uniform() function, as each new shader uniform variable is added, the uniform_array has its space reallocated to insure that the array has enough space for the new entry. Since the new version of the code declares uniform_array to be a vector we can no longer use realloc(3) to add elements to the uniform_array. Instead we use the std::vector methods size() to retrieve the current number of elements in the vector and resize() to increase the number of elements in the vector by 1 (one). After increasing the vector size each of the data fields in the new UNIFORM element of the vector is individually initialized. Arguably, we should have been able to accomplish that task more neatly using the std::vector method push_back(). You should feel free to change the code but shortly I will discuss a change to the code which will make even the use of push_back() unnecessary.
Since uniform_array is no longer a pointer to dynamically allocated space we need to change the destructor so that uniform_array is no longer freed (free(3)). Instead the code uses the std::vector method resize() to release the space.
The changes to vertex_attrib_array parallel the changes made when using uniform_array making their discussion redundant.
These two vectors/arrays (uniform_array and vertex_attrib_array) are populated using a while loop. In my opinion, almost every case where the author uses a while loop and a counter (usually i), the code would be better implemented using an iterated for loop. Let's look at the original loop for populating the uniform_array:
i = 0;
while( i != total )
{
glGetActiveUniform( program->pid,
i,
MAX_CHAR,
&len,
&size,
&type,
name );
PROGRAM_add_uniform( program, name, type );
++i;
}
i = 0;
while( i != total )
{
glGetActiveUniform( program->pid,
i,
MAX_CHAR,
&len,
&size,
&type,
name );
PROGRAM_add_uniform( program, name, type );
++i;
}
In the new code the loop becomes:
for (int i=0; i != total; ++i) {
glGetActiveUniform(this->pid,
i,
MAX_CHAR,
&len,
&size,
&type,
name);
this->add_uniform(name, type);
}
Since the initialization, test, and increment controlling the loop are all together in one place I think this makes the code easier to read, though others may think differently.
As I was writing this post I realized that the old storage and retrieval methods of the location of uniform variables and vertex attribute variables really don't exploit the capabilities of C++. All of the location data are being stored as an array and each time we need to look up the location of a name we have to linearly search through the array. Having realized this I replaced the vectors (arrays) with associative containers, i.e. maps, from the STL; which is why, as discussed above, the use of push_back() becomes unnecessary. Let's look at how to replace uniform_array with a map. I started by removing the name data member from the UNIFORM type:
typedef struct
{
GLenum type;
GLint location;
bool constant;
} UNIFORM;
and I replaced the declaration of the PROGRAM data member uniform_array with
std::map<std::string,UNIFORM> uniform_map;
The method for inserting new UNIFORM variables in the map became:
void PROGRAM::add_uniform(char *name, GLenum type)
{
this->uniform_map[name].type = type;
this->uniform_map[name].location =
glGetUniformLocation(this->pid, name);
this->uniform_map[name].constant = false;
}
Note that this method no longer has a return value. The old method returned the "array index" of the newly added UNIFORM element. Since we're no longer using a linear data structure returning an index isn't meaningful. Remember this also means we need to change the definition of the method in the header file.
The method for retrieving a (shader) uniform variable's location is now:
GLint PROGRAM::get_uniform_location(char *name)
{
auto it = uniform_map.find(name);
return it==uniform_map.end() ?
static_cast<GLint>(-1) :
it->second->location;
}
If you compare these two methods to the versions using std::vector<UNIFORM> I hope you'll agree that they are simpler. One other side benefit is that the code no longer allocates a fixed size area for the name of the uniform variable; this should prevent buffer overflow problems.
Likewise, for managing vertex attribute information I
Likewise, for managing vertex attribute information I
- deleted the name field from the VERTEX_ATTRIB data structure,
- replaced vertex_attrib_array with vertex_attrib_map, again using std::string as a key to do the lookup, and
- modified the add_vertex_attrib() and get_vertex_attrib_location() methods accordingly.
C++ supports several map types. A map can be either ordered or unordered, and require unique keys or allow duplicate keys. For the task of storing the locations of uniform and vertex attribute variables we need to use a map which requires unique keys. But it's not necessary we use an ordered map type like the type I chose above. I haven't benchmarked the mapping code; it may be that the unordered_map type is faster. There are at least four areas of performance we might be concerned with:
Different applications may consider the performance of these areas of different levels of importance. In the various sample applications in the book the maps are only created once and then used, unchanged, for the life of the application so the first criterion probably isn't important for the sample programs. Only the most simplistic of the sample programs look up individual entries by name; in those cases the second criterion is probably the most important. The more complicated sample programs repeatedly iterate over all of the values in the map. I haven't done any benchmarking on any of these criteria so I can't offer any guidance on whether map or unordered_map gives better performance for application use cases. My choice of whether to use map or unordered_map was influenced by how many characters I needed to type and that I can rarely remember the naming conventions for the various associative container subtypes. Hence, I chose map since its name is the shortest and comes readily to mind. You may wish to use a more rigorous method of choosing which type you use to solve similar problems.
Even though my latest version of the code no longer uses std::vectors for the PROGRAM object type the discussion of converting a count and a pointer into a std::vector is still relevant because various other modules in the book require a similar technique and in those instances the data do need to be arranged as a linear data structure. When addressing the changes to those modules in future posts I will skip the discussion of changing a count and an array pointer into an std::vector since the topic has already been covered in this post.
If you have looked at the header file you may have noticed that the add_uniform(), and add_vertex_attrib() methods are now declared private. The add_uniform() and add_vertex_attrib() methods are only ever used by other methods of PROGRAM so they have been hidden from view. Also, note that the destructor uses the std::map method clear() to free the space; we could have (and should have) used clear() in the std::vector version of the code too, rather than resize(0).
Because I have already committed the old code which uses vectors the new code using maps won't be available using the tag chapter2‑1 but the changes will be available eventually.
Since the modified methods for managing the UNIFORM and VERTEX_ATTRIB data structures now use std::string instead of "char[]"/"char*" you might be wondering why I haven't made this change more universally throughout the code. Arguably I should but
One last thing. We aren't done adding methods to the PROGRAM object type. In a future post I'll discuss one of the functions which is part of another module but is really (IMHO) a PROGRAM method.
In my next post I'll talk about the changes in the main application file, SDK/chapter2‑1/templateApp.cpp, and the changes needed in other modules to use the modified versions of the SHADER and PROGRAM code.
- the speed of insertion of new entries,
- the speed of looking up a particular entry,
- the speed of iterating over the entire map, and/or
- the amount of memory needed to implement the map (iOS and Android are both mobile operating systems and may be memory constrained).
Different applications may consider the performance of these areas of different levels of importance. In the various sample applications in the book the maps are only created once and then used, unchanged, for the life of the application so the first criterion probably isn't important for the sample programs. Only the most simplistic of the sample programs look up individual entries by name; in those cases the second criterion is probably the most important. The more complicated sample programs repeatedly iterate over all of the values in the map. I haven't done any benchmarking on any of these criteria so I can't offer any guidance on whether map or unordered_map gives better performance for application use cases. My choice of whether to use map or unordered_map was influenced by how many characters I needed to type and that I can rarely remember the naming conventions for the various associative container subtypes. Hence, I chose map since its name is the shortest and comes readily to mind. You may wish to use a more rigorous method of choosing which type you use to solve similar problems.
Even though my latest version of the code no longer uses std::vectors for the PROGRAM object type the discussion of converting a count and a pointer into a std::vector is still relevant because various other modules in the book require a similar technique and in those instances the data do need to be arranged as a linear data structure. When addressing the changes to those modules in future posts I will skip the discussion of changing a count and an array pointer into an std::vector since the topic has already been covered in this post.
If you have looked at the header file you may have noticed that the add_uniform(), and add_vertex_attrib() methods are now declared private. The add_uniform() and add_vertex_attrib() methods are only ever used by other methods of PROGRAM so they have been hidden from view. Also, note that the destructor uses the std::map method clear() to free the space; we could have (and should have) used clear() in the std::vector version of the code too, rather than resize(0).
Because I have already committed the old code which uses vectors the new code using maps won't be available using the tag chapter2‑1 but the changes will be available eventually.
Since the modified methods for managing the UNIFORM and VERTEX_ATTRIB data structures now use std::string instead of "char[]"/"char*" you might be wondering why I haven't made this change more universally throughout the code. Arguably I should but
- the task's priority hasn't risen high enough on my "to do" list;
- if I'm going to change the way strings are stored I want to consider supporting more than just ASCII (or even Latin 1, aka ISO/IEC 8859-1) strings; I want to consider if the string type should be UTF‑8*, UTF‑16, Unicode, or some other encoding and I haven't spent enough time researching the tradeoffs and the compatibility of these various encodings with the various data files a graphics application may need to process (such as Wavefront, Blender, Ogg Vorbis, and MD5 files which are covered later in the book), and what string encodings can be used to access files with names in non‑English/non‑Western European languages; and
- many of these data structures contain data which came from an OpenGL function, and/or must be passed into an OpenGL function and since OpenGL is a C API the data passed into and out of the OpenGL functions must be held in C data types (which may mean that string is a suitable substitute for "char[]"/"char*" because the string type has the c_str() method but data types which support more than ASCII and/or Latin 1 may not be suitable).
One last thing. We aren't done adding methods to the PROGRAM object type. In a future post I'll discuss one of the functions which is part of another module but is really (IMHO) a PROGRAM method.
In my next post I'll talk about the changes in the main application file, SDK/chapter2‑1/templateApp.cpp, and the changes needed in other modules to use the modified versions of the SHADER and PROGRAM code.