Data-Driven Graphics Pipeline, Part 1: GPU Resource Generalization

A topic I’ve never really been able to read a lot on is how one sets up their CPU-side pipeline. I’m not talking about how you treat your objects, but the actual definition of each pass, deciding what objects it will handle, resources it needs, etc. Up to about a month ago, my pipeline had remained pretty unchanged throughout the development of our game engine. It boiled down to code that looked like this:

///////////////////////////////////////////
// rendering all scene geometry for gbuffer
// create debug event
dxCore->StartDebugEvent("Gbuffer Pass");

// prep for 3D model pass, setting states, constant buffers
PrepFor3DModels( view, proj );

// for each draw call, call render
while(m_drawList[ currentIndex ].Shader_ == SHADER_DEFERRED_GEOMETRY)
  Render3DModel( m_drawList[ currentIndex++ ], currentCamera );

// stamp model rendering in the profiler
STAMP( "Model Rendering" );

// end this debug event
dxCore->EndDebugEvent( );

This was my definition of a pass. There were about twelve of these, all which defined different parts my deferred pipeline. It’s okay, not exactly a testament to my programming skills, but it got the job done. When I went to implement shadows, my architecture started to fracture. My pipeline was extremely brittle, hard to work with, forced a ton of long recompilation and was extremely prone to bugs when I introduced new features. A large part of this was due to the fact that although I had managers for all the different parts of my graphics engine (depth-state manager, render-target manager, etc), there was no management system for render passes.

This left it to me to make sure that all of the states and resources were set correctly on the GPU. I had to make sure that everything was set up properly; if I didn’t, a prior pass could set weird states for the following pass, causing crazy results. Tacking on the future plan for cool effects and such, and I had an immense feeling of dread building in my stomach.

With all of this, I decided that a change was in order. I grabbed a whiteboard and started to plan out what I wanted this to look like. I started with the features that I wanted:

  1. Automatic handling of GPU state-changes.
  2. Automatic handling of resources.
  3. Safety checking for conflicting GPU resources (bound to same slot, targets on input and output simultaneously, etc).
  4. Automatic handling of all debugging systems (profiling targets, DirectX debugging semantics, assertions).
  5. Generalization of object handling (how does this pass handle the entities it processes?).
  6. Easy to assemble/modify any part of the pipeline.

From this starting point, I started writing out the different parts I was going to need. Sprawled all over the whiteboard, this seemed like a fairly hefty task. Even before I started, I saw a lot of problems:

  • How could generalizing buffers work? Different types, slots, shaders, all needing to be handled.
  • There was no way to generalize how I handle different objects, AKA handling a directional light versus a model.
  • Multiple render passes?
  • All of the really weird passes that were not normal (point-sprites not having a vertex buffer and a couple others).
  • Flexibility to work outside of the system, if I came up with a really different idea for a render pass.

These are just a few of the many problems, but you get the idea. The very first one I tackled was generalizing my GPU resources; textures, input render targets, constant buffers, etc. I wanted to have an easy way of managing resources while also doing stuff like error checking for resources having slot collisions.

Starting off, I broke my resources into two groups: constant buffers, and everything else. Constant buffers would have to be templated, since they vary so drastically from buffer to buffer. All of my other resources could be referenced with an ID, so I only needed one class to handle all of them. Going from this, I decided what information both types of resources would have in common. This ended up being the slot index, the resource-type ID, and the shader that I wanted to give this resource to.


// enum used to define what resource this shader resource has
enum ResourceType : unsigned
{
    RESOURCE_CBUFFER,
    RESOURCE_MODEL,         // vert and index buff as a models
    RESOURCE_TEXTURE,       // texture buffer
    RESOURCE_INPUT_RT,      // render target as texture
    RESOURCE_INPUT_DEPTH,   // depth target as texture
    RESOURCE_COUNT
};

class GlobalShaderResource
{
public:
    GlobalShaderResource(ShaderSlot slotIndex, ResourceType resourceType, SHADERTYPE targetShader);
    virtual ~GlobalShaderResource(void);

    // map this resource to the GPU in a specified slot
    virtual void Map(void);

    // returns true if two resources collide
    bool operator=(const GlobalShaderResource &rhs) const;

    // method used for other checks
    ShaderSlot GetSlotIndex(void) const;
    static void SetGfxMgr(GfxManager *mgr);

protected:
    static GfxManager *m_manager;   // the manager

    ShaderSlot   m_slotIndex;       // where to map
    ResourceType m_resourceType;    // what resource is this?
    SHADERTYPE   m_targetShader;    // what shader expects this resource
};

The constructor never gets called explicitly, but takes the three pieces of information I just mentioned. The Map method gets overridden by the respective resource type, and the equality operator is for collision checking.

Moving forward, I implemented the class for my constant buffers:

template<typename T, BUFFER_LIST BufferType>
class GlobalCBuffer : public GlobalShaderResource
{
public:
    GlobalCBuffer(SHADERTYPE shader, ShaderSlot shaderSlotIndex = static_cast<ShaderSlot>(BufferType));
    virtual ~GlobalCBuffer(void);

    // update this data to prepare for GPU usage
    void Update(const T &data, ShaderSlot slotIndex = SHADER_SLOT_COUNT);

    // map data to the GPU
    void Map(void) override;

private:
    T m_bufferData;  // the data we track for
};

Templated to take the type of the buffer, as well as an enum that is used to access the GPU-side buffer. The constructor takes the shader, and assumes that the slot of the buffer is the same as the buffer enum. The Update method is straightforward; taking a resource, and allowing us to change where it is mapped, if we desire.

Finally, we get to our other GPU resources:

class GlobalGPUResource : public GlobalShaderResource
{
public:
    GlobalGPUResource(ShaderSlot shaderSlot, ResourceType type, SHADERTYPE shadertype = static_cast<SHADERTYPE>(6));
    virtual ~GlobalGPUResource(void);

    // update whatever is on the gpu
    void Update(unsigned resourceID);

    // map data
    void Map(void) override;

private:
    int m_resourceID;
};

The constructor takes similar information as the constant buffer, with the exception that it takes a ResourceType. Update simply takes the ID of the resource, and Map uses this ID to query the proper manager for the shader resource.

That about sums up how I handle global resources, in my next post I’ll talk about how I handled rendering different object types.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s