75
5
DelayingOpenGLCalls
Patrick Cozzi
Analytical Graphics, Inc.
5.1Introduction
It is a well known best practice to write an abstraction layer over a rendering API
such as OpenGL. Doing so has numerous benefits, including improved portabil-
ity, flexibility, performance, and above all, ease of development. Given
OpenGL’s use of global state and selectors, it can be difficult to implement clean
abstractions for things like shader uniforms and frame buffer objects. This chap-
ter presents a flexible and efficient technique for implementing OpenGL abstrac-
tions using a mechanism that delays OpenGL calls until they are finally needed at
draw time.
5.2Motivation
Since its inception, OpenGL has relied on context-level global state. For exam-
ple, in the fixed-function days, users would call
glLoadMatrixf() to set the en-
tries of one of the transformation matrices in the OpenGL state. The particular
matrix modified would have been selected by a preceding call to
glMatrix-
Mode()
. This pattern is still in use today. For example, setting a uniform’s value
with
glUniform1f() depends on the currently bound shader program defined at
some earlier point using
glUseProgram().
The obvious downside to these selectors (e.g., the current matrix mode or the
currently bound shader program) is that global state is hard to manage. For ex-
ample, if a virtual call is made during rendering, can you be certain the currently
bound shader program did not change? Developing an abstraction layer over
OpenGL can help cope with this.
76 5.DelayingOpenGLCalls
std::string vertexSource = // ...
std::string fragmentSource = // ...
ShaderProgram program(vertexSource, fragmentSource);
Uniform *diffuse = program.GetUniformByName("diffuse");
Uniform *specular = program.GetUniformByName("specular");
diffuse->SetValue(0.5F);
specular->SetValue(0.2F);
context.Draw(program, /* ... */);
Listing 5.1. Using an OpenGL abstraction layer.
Our goal is to implement an efficient abstraction layer that does not expose
selectors. We limit our discussion to setting shader uniforms, but this technique is
useful in many other areas, including texture units, frame buffer objects, and ver-
tex arrays. To further simplify things, we only consider scalar floating-point uni-
forms. See Listing 5.1 for an example of what code using such an abstraction
layer would look like.
In Listing 5.1, the user creates a shader program, sets two floating-point uni-
forms, and eventually uses the program for drawing. The user is never concerned
with any globals like the currently bound shader program.
In addition to being easy to use, the abstraction layer should be efficient. In
particular, it should avoid needless driver CPU validation overhead by eliminat-
ing redundant OpenGL calls. When using OpenGL with a language like Java or
C#, eliminating redundant calls also avoids managed to native code round-trip
overhead. With redundant calls eliminated, the code in Listing 5.2 only results in
two calls to
glUniform1f() regardless of the number of times the user sets uni-
form values or issues draw calls.
diffuse->SetValue(0.5F);
specular->SetValue(0.2F);
context.Draw(program, /* ... */);
diffuse->SetValue(0.5F);
specular->SetValue(0.2F);
context.Draw(program, /* ... */);
context.Draw(program, /* ... */);
Listing 5.2. An abstraction layer should filter out these redundant calls.
5.3PossibleImplementations 77
5.3PossibleImplementations
Now that we know what we are trying to achieve, let’s briefly consider a few
naive implementations. The simplest implementation for assigning a value to a
uniform is to call
glUniform1f() every time a user provides a value. Since the
user isn’t required to explicitly bind a program, the implementation also needs to
call
glUseProgram() to ensure the correct uniform is set. This is shown in List-
ing 5.3.
void SetValue(float value)
{
glUseProgram(m_handle);
glUniform1f(m_location, value);
}
Listing 5.3. Naive implementation for setting a uniform.
This implementation results in a lot of unnecessary calls to glUseProgram()
and
glUniform1f(). This overhead can be minimized by keeping track of the
uniform’s current value and only calling into OpenGL if it needs to be changed,
as in Listing 5.4.
void SetValue(float value)
{
if (m_currentValue != value)
{
m_currentValue = value;
glUseProgram(_handle);
glUniform1f(_location, value);
}
}
Listing 5.4. A first attempt at avoiding redundant OpenGL calls.
Although epsilons are usually used to compare floating-point values, an exact
comparison is used here. In some cases, the implementation in Listing 5.4 is suf-
ficient, but it can still produce redundant OpenGL calls. For example, “thrashing”
occurs if a user sets a uniform to
1.0F, then to 2.0F, and then back to 1.0F be-
78 5.DelayingOpenGLCalls
fore finally issuing a draw call. In this case, glUseProgram() and glUni-
form1f()
would be called three times each. The other downside is that
glUseProgram() is called each time a uniform changes. Ideally, it would be
called only once before all of a program’s uniforms change.
Of course, it is possible to keep track of the currently bound program in addi-
tion to the current value of each uniform. The problem is that this becomes diffi-
cult with multiple contexts and multiple threads. The currently bound program is
context-level global state, so each uniform instance in our abstraction needs to be
aware of the current thread’s current context. Also, tracking the currently bound
program in this fashion is error prone and susceptible to thrashing when different
uniforms are set for different programs in an interleaved manner.
5.4DelayedCallsImplementation
In order to come up with a clean and efficient implementation for our uniform
abstraction, observe that it doesn’t matter what value OpenGL thinks a uniform
has until a draw call is issued. Therefore, it is not necessary to call into OpenGL
when a user provides a value for a uniform. Instead, we keep a list of uniforms
that were changed on a per-program basis and make the necessary
glUni-
form1f()
calls as part of a draw command. We call the list of changed uniforms
the program’s dirty list. We clean the list by calling
glUniform1f() for each
uniform and then clear the list itself. This is similar to a cache where dirty cache
lines are flushed to main memory and marked as clean.
An elegant way to implement this delayed technique is similar to the observ-
er pattern [Gamma et al. 1995]. A shader program “observes” its uniforms.
When a uniform’s value changes, it notifies its observer (the program), which
adds the uniform to the dirty list. The dirty list is then cleaned as part of a draw
command.
The observer pattern defines a one-to-many dependency between objects.
When an object changes, its dependents are notified. The object that changes is
called the subject and its dependents are called observers. For us, the situation is
simplified: each uniform is a subject with only one observer—the program. Since
we are interested in using this technique for abstracting other areas of OpenGL,
we introduce the two generic interfaces shown in Listing 5.5.
The shader program class will implement
ICleanableObserver, so it can
add a uniform to the dirty list when it is notified that the uniform changed. The
uniform class will implement
ICleanable so it can call glUniform1f() when
the dirty list is cleaned. These relationships are shown in Figure 5.1.
5.4DelayedCallsImplementation 79
class ICleanable
{
public:
virtual ~ICleanable() {}
virtual void Clean() = 0;
};
class ICleanableObserver
{
public:
virtual ~ICleanableObserver() {}
virtual void NotifyDirty(ICleanable *value) = 0;
};
Listing 5.5. Interfaces used for implementing the observer pattern.
Let’s first consider how to implement the class ShaderProgram. As we saw
in Listing 5.1, this class represents a shader program and provides access to its
uniforms. In addition, it cleans the dirty list before draw commands are issued.
The relevant parts of its implementation are shown in Listing 5.6. The shader
program keeps two sets of uniforms: one set for all the active uniforms, which is
accessed by uniform name (
m_uniforms), and another set for just the dirty uni-
forms (
m_dirtyUniforms). The dirty uniforms set is a std::vector of IClean-
able
pointers, since the only operation that will be applied to them is calling
Figure 5.1. Relationship between shader programs and uniforms.
ICleanable
Uniform ShaderProgram
ICleanableObserver
implementsimplements
has zero to many
(static)
has zero to many
(dynamic)
has one
(weak reference)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.12.50