TSE/Batching

From TDN

TGEA Batching

Contents

Overview

To improve the engine's rendering behavior a new set of rendering classes was added. The goal of these rendering classes is to group geometry with the same material / render states together so they get rendered at the same time. This improves the batches that are sent to the graphics card / driver which improves performance. The size of these batches are normally a large bottleneck in a system. Large batches are better!

Important Data Structures / Classes

  • RenderInstance: This class is used to represent a "renderable thing". This contains all of the scene state needed to render, and often the geometry itself.
  • RenderElemManager and descendants: These classes sort and render a list of RenderInstances. The primary purpose of these classes is to deal with setting render states and driving the geometry submission to the underlying API.
  • gRenderInstManager: This is a global singleton object of type RenderInstManager which is used to create render instances, and maintains a list of RenderElemManager. This list determines the order which objects get drawn. This is the place to tweak when the sky renders vs. the terrain etc. Most of the work done by this object is accomplished by looping through the list of RenderElemManagers and calling the same method. The sort, render, and clear methods work this way.

Code flow

  1. The scenegraph calls ::prepRenderImage for each SceneObject that is in the current scope.
  2. prepRenderImage determines if it should render or not. If it does, it creates a RenderInst object (using gRenderInstManager.allocInst()). It sets the various properties of that object, then finally submits it to the gRenderInstManager via the addInst method. The addInst method adds the renderinstance to the appropriate RenderElemManagers.
  3. gRenderInstManager.sort is called to sort RenderInstances by their material type. This is done to minimize the number of times we need to switch shaders which is one of the most expensive things we can do with the GPU.
  4. gRenderInstManager.render is called.
  5. gRenderInstManager.clear is called and the process starts over.

Render Instance Types

There are basically two styles of RenderInstances: objects and meshes. (This is a simplification, but enough to get the point across.)

  1. RIT_Object, this basically tells the RenderManager to call back into the object via the ::renderObject method and the object takes over rendering. This is useful for porting older code with minimal changes. Or for interfacing with third party code. Examples of RIT_Object based classes are Atlas, fxSunLight, and the Terrain.
  2. RIT_Mesh, this is submitted to the ::addInst method with the vertex and primitive buffers setup already. The RenderInstance then becomes all of the information needed to render geometry. For the long term, rendering this way is definitely preferred. Its more data driven and will allow more flexibility in the future. ShapeBase and Interiors are examples that use this method of rendering.

Render loop

Finally, the ::render method of the RenderElemManager itself deserves a quick look. Here are some snippets of the RenderMeshMgr::render method with more explanation:

This is looping through each RenderInst assigned to this RenderElemMgr that was submitted in step 2 of above. Notice how we are not incrementing j here, this is done later on in the code.

  for( U32 j=0; j<binSize; )
  {

Then there will be code in here for setting up the SceneGraphData class that the Material system uses to set shader constants and setting up other global state like texture filtering.

This code below sets up each pass of material. The ::setupPass method sets textures and shader constants.

     while( mat && mat->setupPass( sgData ) )
     {

This code loops from the current position in the render list to (potentially) the end of the list

        for( a=j; a<binSize; a++ )
        {

This is very important! This code checks to see if the render instance is using the same materal! If not, we need to exit out of this inner loop and go back to the top to call mat->setupPass for this new material

           if( mat != passRI->matInst )
           {
              break;
           }

Then shader constants and render state that may change per object is set. If you are adding shader constants to support a custom effect, you may need to add code here if that constant will change per object.

Finally we draw the geometry with a good ole drawPrimitive call.

           // set buffers if changed
           if( lastVB != passRI->vertBuff->getPointer() )
           {
              GFX->setVertexBuffer( *passRI->vertBuff );
              lastVB = passRI->vertBuff->getPointer();
           }
           if( lastPB != passRI->primBuff->getPointer() )
           {
              GFX->setPrimitiveBuffer( *passRI->primBuff );
              lastPB = passRI->primBuff->getPointer();
           }
           // draw it
           GFX->drawPrimitive( passRI->primBuffIndex );

Here's where we reset our position in the bin list. Basically, move it one if we only processed one item, or move it to the last item we've processed.

     // force increment if none happened, otherwise go to end of batch
     j = ( j == matListEnd ) ? j+1 : matListEnd;

Summary

So, the overall effect this has is that passes of the same material are drawn together, which makes our batches larger. So if you have 50 orcs in a scene, you'll see 50 diffuse/sunlight passes draw, then 50 dynamic light passes draw (assuming each orc is affected by the same light).