Writing a new SPU

This web page will step you through the creation of the "invert" SPU.  This SPU will modify the OpenGL stream so that all of the colors are inverted.  Although this is a toy example, it shows the power of Chromium's modularity, and can be used as a starting point for more complex SPUs.

Step 1: Creating the SPU directory

This step is very simple.  Change to the spu/ directory, and type the following commands:

cp -r template invert
cd invert
python gen_template.py invert

The first command will make a copy of the "template" SPU in a directory spu/invert/.  The gen_template.py script will rename all of the template files to be consistent with your chosen SPU name (in this case, "invert"), and it will also change all of the variable names and other identifiers in the C code and header files to be consistent with the new SPU name.

This process will leave you with seven files:

Assuming that the rest of the Chromium system has been built already, you should be able to type "make" at this point, and the SPU should compile without warnings.  If it doesn't, something is horribly wrong.

Step 2: Initialize the SPU

For the most part, this SPU initializes itself.  However, there are two small changes to be made to the initialization routine.

First, we must introduce the notion of SPU inheritance.  A SPU need not implement the full OpenGL API.  In fact, most SPUs will not.  Instead, a SPU can implement a subset of the OpenGL API, and the remainder of the API can be obtained from a "SuperSPU".  For example, a SPU that is designed to only accept calls to glDrawPixels could get the rest of its API from the error SPU, which will print an error message on any OpenGL function call.  The error SPU is in fact the default SuperSPU.

In the case of the Invert SPU, we will inherit from the passthrough SPU, since we want to modify an OpenGL stream as it is made.  To do this, edit the file invertspu_init.c.  In the function SPULoad, you will see the line:

super = NULL;

Change this line to:

super = "passthrough";

That's all there is to it.  If you've been paying really close attention, you might think that we need to check to make sure this SPU is loaded with a child (i.e., it does not appear at the end of a SPU chain).  This check is actually performed by the passthrough SPU when it is loaded, so another check would be redundant.

Notice two other things about this initialization file, and in particular the function SPUInit:

  1. There is a global variable called invert_spu that holds all information pertaining to this instance of this SPU.
  2. A copy of the dispatch tables for the SPU's SuperSPU, and this SPU's child (i.e., the SPU following it in the chain) are stored in the invert_spu global variable.  This is how we will pass functions through once we have modified their arguments.

You should still be able to compile the invert SPU after this step.

Step 3: Tell the SPU which functions you're going to implement

Open the file invertspu.c.  You will see the following code at the bottom of the file:

SPUNamedFunctionTable _cr_invert_table[] = {
    { NULL, NULL }
};

This table is a NULL-terminated list of  { name, function }  pairs that will be parsed by the SPU loader to build a SPU dispatch table.  Therefore, order does not matter.  For this SPU, we will want to implement those functions that affect the color.  There are actually a lot of them, so for the purposes of this example, we'll only implement a few: glColor3f, glClearColor, and glMaterialfv.  For this step, all we have to do is add these function names and pointers to our implementation in this list.  Change the above code to:

SPUNamedFunctionTable _cr_invert_table[] = {
    { "Color3f",    (SPUGenericFunction) invertColor3f },
    { "ClearColor", (SPUGenericFunction) invertClearColor },
    { "Materialfv", (SPUGenericFunction) invertMaterialfv },
    { NULL, NULL }
};

Notice two things about this table:  First, the function names on the left do not begin with "gl".  Second, the functions on the right (which we haven't implemented yet) must be cast to "SPUGenericFunction" in order to properly build this table.  This has the unfortunate side effect that you can easily get the number or type of arguments to a function wrong, and the compiler will not catch the error, so be careful.

After this step, you will not be able to compile the SPU, because the functions invertColor3f, invertClearColor, and invertMaterialfv are not defined.

Step 4: Define the functions

We will implement the three needed functions at the top of the file invertspu.c, since that's where they are referred to.  In a larger SPU, it you would probably want to break these things into multiple files.

Immediately before the table you created in step 3, add the lines:

void INVERTSPU_APIENTRY invertColor3f( GLfloat red,
                                       GLfloat green,
                                       GLfloat blue )
{
}

(We will fill in the body in a minute).  The strange identifier "INVERTSPU_APIENTRY", defined in the header invertspu.h, is necessary to ensure correct operation on Windows.  On Windows, it is defined to "__stdcall", which matches the calling conventions used by the system's OpenGL implementation.  This is necessary since the OpenGL replacement DLL will simply jump to this function instead of calling it, so the calling conventions need to match those expected by the calling application.  On non-Windows machines, this identifier has no effect.  See the header file for the entire definition.

Important: Every OpenGL replacement function needs to use this identifier.

The definition of the other two functions is similarly straightforward:

void INVERTSPU_APIENTRY invertClearColor( GLfloat red,
                                          GLfloat green,
                                          GLfloat blue,
                                          GLfloat alpha )
{
}

void INVERTSPU_APIENTRY invertMaterialfv( GLenum face,
                                          GLenum mode,
                                          const GLfloat *param )
{
}

Note that the SPU may still not compile at this point, because the unused variables may generate warnings (depending on your compiler), and the default Chromium build system configuration files (in config/) consider warnings to be errors.

Step 5: Write the function bodies

The final step in our SPU is to actually implement our filters.  Let's look at the implementation of glColor3f first.

All we need to do is subtract the specified colors from unity before passing them to our child.  Since our SuperSPU already handles passing parameters to a child SPU, all we need to do is call our SuperSPU's implementation of Color3f with modified arguments. Therefore, the full body for invertColor3f should look like:

void INVERTSPU_APIENTRY invertColor3f( GLfloat red,
                                       GLfloat green,
                                       GLfloat blue )
{
    invert_spu.super.Color3f( 1-red, 1-green, 1-blue );
}

That's all there is to it!  Note that this would have been exactly equivalent to calling:

invert_spu.child.Color3f( 1-red, 1-green, 1-blue );

But I prefer calling the passthrough SPU as a (marginally) cleaner design.

The implementation of invertMaterialfv is slightly more interesting, because it needs to behave differently depending on what the "mode" parameter is.  In particular, if mode is GL_SHININESS, there is only one parameter, and we shouldn't invert it.  Otherwise, there are four parameters, and they should get inverted.  To do this, we make a local array of four float variables, invert the provided parameters, and pass them to the SuperSPU. If the user specifies the material shininess, we leave the parameters alone and just dispatch them to the SuperSPU verbatim:

void INVERTSPU_APIENTRY invertMaterialfv( GLenum face,
                                          GLenum mode,
                                          const GLfloat *param )
{
     if (mode != GL_SHININESS)
     {
        GLfloat local_param[4];
        local_param[0] = 1-param[0];
        local_param[1] = 1-param[1];
        local_param[2] = 1-param[2];
        local_param[3] = 1-param[3];

        invert_spu.super.Materialfv( face, mode, local_param );
     }
     else
     {
        invert_spu.super.Materialfv( face, mode, param );
    }
}

The implementation of invertClearColor is almost identical to the implementation of invertColor3f, and is not shown here.

Step 6: Test it out!

All that's missing at this point is a configuration script that references our new SPU.  For this example, we'll just modify the crdemo.conf file that was described in the "Configuration Scripts" section of this documentation.  Change directories to mothership/configs/ and copy crdemo.conf to invert.conf.

All that's required is a single call to the AddSPU method of the client node.  Around lines 17 and 18, where the server and client SPUs are created, add the line:

invert_spu = SPU( 'invert' )

Then, immediately before the client SPU is added to the client node (line 29 in the unmodified crdemo.conf), add the line:

client_node.AddSPU( invert_spu )

It is important that the invert SPU comes before the client SPU in the configuration script, since that is the SPU chain order that will be created when the application starts up.  Your resulting script should look like this.

"bluepony" without the Invert SPU "bluepony" with the Invert SPU

Step 7: Overriding default OpenGL state

OpenGL is a tricky business, and getting simple things right can sometimes be a little hard.  For example, what if the user never sets the color, material, or clear color, and simply uses the defaults?  Surely we'd want those colors to be inverted too, right? The solution is to override the default OpenGL state.

At this point we must consider OpenGL rendering contexts. A rendering context keeps track of all OpenGL state (such as current color, lighting state, blending, textures, etc). Application programs must create at least one rendering context (with glXCreateContext, for example) and bind a context (with glXMakeCurrent) before drawing anything. Furthermore, an application may create many rendering contexts and bind them at different times to different windows.

If we want to override the default state in an OpenGL rendering context, the time to do it is immediately after we bind the context for the first time. That means we have to override Chromium's MakeCurrent function.

Here's an example that overrides the default color, material and clear color:

void INVERTSPU_APIENTRY invertMakeCurrent( GLint crWindow, GLint nativeWindow, GLint ctx )
{
    static GLfloat diffuse[4] = { 1-0.8f, 1-0.8f, 1-0.8f, 1.0f };
    invert_spu.super.MakeCurrent( crWindow, nativeWindow, ctx );
    invert_spu.super.Color3f( 0, 0, 0 ); // default color is white
    invert_spu.super.ClearColor( 1, 1, 1, 1 ); // default clear color is black
    invert_spu.super.Materialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse );
}

Note: in this example we're actually overriding the default state every time MakeCurrent is called. That's OK if only a little state is being set. Otherwise, we'd have to keep track of the context (ctx) parameter and only set the state when we saw a new context handle.

The crWindow parameter is Chromium's internal window ID number. It's typically a small integer.

The nativeWindow parameter is the handle of the corresponding native window system window (i.e. an X Window ID).

The ctx parameter is Chromium's internal context ID. It's typically a small integer.

We might have alternately called the functions from step 5 with the OpenGL defaults, so we wouldn't have inverting logic in too many places.  Also, careful implementors will want to invert the default specular, emission, and ambient colors as well.

A good program to test this SPU with is the "bluepony" demo that comes with GLUT.  Notice that the pony is a different color, as is the clear color.  However, the floor is still the same, because those colors were specified with glColor4fv, which we did not implement.  For an example of how to write this SPU in a more complete and correct way, see "Automatically generating code for a SPU".