Shaders and Metaprogramming

Published July 03, 2007 by Promit Roy, posted by Promit
Do you see issues with this article? Let us know.
Advertisement

Introduction

There has been a lot of discussion about whether managed code is "good enough" and "fast enough" for writing graphics code and games in. Many people have touted its ability to make developing games faster and easier. However, there hasn't been much discussion about specific ways to more effectively leverage the new abilities that managed code gives us. There are some very interesting advantages to working on Microsoft's .NET platform, which can be applied to graphics programming in order to solve some difficult problems.

The problem I'll be examining in this article is how to robustly expose shader constant parameters from an application. Many shader constants in modern games are data driven; they are pulled from material files and the like. However, some constants are not data driven but are inherent to the code itself. The model, view, and projection matrices are good examples. The rendering pipeline computes these matrices as part of rendering; they are not part of the data that is loaded from file. When dealing with these values, there are generally 4 steps that need to be taken to handle such a constant:

  1. Store the value in the application somewhere
  2. Set the value of that parameter on the application side
  3. Check if a given shader uses that parameter, and obtain an API handle to the parameter if it does
  4. Propagate the parameter's value to the shader at the appropriate time

Every time we add or remove a new constant parameter, we need to touch code in four different places. This is error prone and generally just a total pain to deal with. While steps 1 and 2 can't really be avoided, 3 and 4 are irritatingly repetitive. The code required to deal with step 3 usually looks something like this:


this.ModelHandle = shader.GetParameter("Model");
this.ViewHandle = shader.GetParameter("View");
this.ModelViewProjHandle = shader.GetParameter("ModelViewProj");
this.ColorHandle = shader.GetParameter("Color");
...

The code is quite repetitive, but the differences between each line are just enough to make refactoring difficult. We can try to attack the problem by storing a table of parameter handles that are keyed by string. Then we could simply iterate over the table to initialize all of the handles. However, that requires us to maintain some sort of hard coded string table of all of the available parameters, which isn't much of an improvement over this version. And it still won't help us in simplifying the code required for step 4:


if(this.ModelHandle != null )
        shader.SetValue(this.ModelHandle, Constants.Model);
if(this.ViewHandle != null )
        shader.SetValue(this.ViewHandle, Constants.View);
if(this.ModelViewProjHandle != null )
        shader.SetValue(this.ModelViewProjHandle, Constants.ModelViewProj );
if(this.ColorHandle != null )
        shader.SetValue(this.ColorHandle, Constants.Color);
...

The table we wanted to use in step 3 doesn't help us because we can't use those string keys to look up members of the Constants class that we're using to hold all of our shader parameters.

Or can we?

Reflection

Reflection is a particularly powerful ability of code that is running on the .NET platform. It allows us to examine the structure of our classes, getting information about what members they contain, what metadata (attributes) has been attached, and more. There's a lot of information out there about reflection, and I won't cover it in detail here.

So how can we make reflection work for us? Remember that I discussed hard coding a table of strings and parameter handles. We can use reflection to make that table for us. It allows us to obtain all of the member variables of a class as an array. By consolidating that information into a Constants class (as appears in the pseudo-code for step 4), we can easily create that table without being forced to hard code it alongside the actual member variables.


Type type = typeof( ConstantMap );
FieldInfo[] fields = type.GetFields( BindingFlags.Public | BindingFlags.Instance );

Now we have an array describing all of our available constants. We can use the Name property on FieldInfo to get the string name for the field, and we can also use it to access the value of the field for a given instance. Now we can switch over steps 3 and 4 to using much simpler loops to do their work. For step 3:


for( int i = 0; i  = shader.GetParameter( fields.Name )
{
}

And for step 4 (note that some type casting details have been omitted):


for( int i = 0; i  != null) 
{
	shader.SetValue(this.Handles, fields.GetValue(Constants);
}

Now, all we have to do to add a new constant parameter is to add it as a member of ConstantMap, and update its value appropriately. The reflection and loops will automatically deal with looking it up as a shader parameter, and propagating its value down to the shader. Problem solved, right?

Unfortunately, no. It's not quite that simple. Reflection is not especially fast. In particular, that GetValue function takes a very long time compared to the direct member access which we were using in the hard coded version. And because this is something that will be called thousands of times per frame, we can't be wasting so much time just accessing members of a class. Luckily, .NET still has one trick up its sleeve.

Metaprogamming

Metaprogramming refers to writing code which itself writes new code. The term is very broad, and anything that creates new code (even outputting text to a BAT file and running it) counts. The type of metaprogramming we'll be doing was added for .NET 2.0, and is called DynamicMethod. (You can accomplish what I'm about to describe using purely 1.1 functionality, but it's a little more involved.) DynamicMethod provides an extremely lightweight way to build functions at runtime. You can write CIL byte codes to it (these are the byte codes that .NET natively runs), and then use it to create a callable delegate, which is as fast to invoke as a virtual function call. The first time it's called, all of the byte code will be run through the JIT and converted to native code, and there will be no overhead after that. Setting up the DynamicMethod involves providing a description of the function it defines, and then retrieving an ILGenerator object which allows us to write the actual CIL. After that, we write our byte code, and then create the actual delegate object and store it. The code ends up looking something like this:


DynamicMethod dynMethod = new DynamicMethod( "SetConstants", typeof( void ),
        new Type[] { typeof( Effect ),
        typeof( EffectHandle[] ), typeof( ConstantMap ) },
        typeof( ConstantMap ), true );
ILGenerator ilGen = dynMethod.GetILGenerator();

//Use the ILGenerator to write all of the bytecode here
m_SetConstants = (SetConstantsDelegate) dynMethod.CreateDelegate( 
        typeof( SetConstantsDelegate ) );

Our goal is to recreate the hard coded version of the shader binding. First, the reflection API will generate a complete list of available shader parameters for us. Then, we will create a DynamicMethod that runs through each parameter, checks if the shader uses that parameter, and propagates the value if it does. The final generated byte code will be completely identical to the original hard coded version, but we won't be the ones who wrote it, and it will automatically update as members are added or removed from the ConstantMap class. (To get a feel for how CIL looks, I'd suggest playing with the ILDasm tool that is included in the .NET Framework SDK.)

The full code is too long to show here; see the attached file for an example using Managed DirectX and the Effect framework. All of the heavy lifting is done in the static constructor for ConstantMap. It goes through each member field, storing its name in a string array and writing the byte code to test for null and call SetValue for each field. Another helper is also provided that simply generates an array of all of the parameter handles for a shader and returns it. This is stored along with the shader and then passed back to the ConstantMap along with the shader just before rendering.

Using and maintaining this system is exceedingly simple. To add a shader constant parameter, add a new member to ConstantMap. To remove the parameter, remove the member. Clients of this class only have three responsibilities. The first is to update the values in the constant map. Second, they need to store the parameter handle array returned by ConstantMap.LoadParameters() for each shader. Lastly, they need to pass the shader and its accompanying parameter array to the ConstantMap.SetConstants(). Everything else is handled by the ConstantMap itself. Getting an array of constant parameter handles is a single line of code:


EffectHandle[] effectParams = Constants.LoadParameters(effect);

And after updating all of the parameter values, setting up the shader is also a single line:


Constants.SetConstants(effect, effectParams);

Problem Solved

What we have now is an extremely robust and efficient system for exposing shader constants from the rendering pipeline to shaders. All of the expensive reflection work is done during application startup, where the cost is relatively miniscule. At runtime, there is no work being done that wasn't being done in the hard coded version. Yet with zero performance overhead, we've managed to make huge gains in terms of reliability and ease of use.

There is still room for improvement, though. Right now, it's assumed that all the data is contained within the ConstantMap itself. This means that some situations could require a lot of data to be copied. You can adapt the code generation so that it can handle members that are reference types that expose shader data. This way, data could be stored elsewhere and referenced rather than copied. It's also possible to add lazy evaluation. For example, not every shader is going to need the ModelInverseTranspose matrix, so computing it repeatedly is a waste of time. The code generation could conceivably be set up so that it only computes these quantities when they are actually used by the current shader, and those values could even be cached. This design forms an extremely flexible base on which to build simple or sophisticated shader systems.

Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Discusses how to robustly expose shader constant parameters from a .NET 2.0 application using the reflection and metaprogramming abilities of managed code

Advertisement

Other Tutorials by Promit

Advertisement