GLContextData

One of the main goals of the Vrui toolkit is to support development of truly portable VR software. This means a VR application is written once, and then runs on any VR environment without change -- including desktop environments, single-computer VR environments, and even multi-pipe or cluster-based VR environments. The GLContextData class (and its sibling, the GLObject class) provide a framework to hide differences between single-pipe and multi-pipe or cluster-based OpenGL rendering from a developer.

The exact problem that GLContextData/GLObject try to hide is how to store per-context OpenGL data in an application that might run in a single- or multi-pipe environment depending on from where it is started. Take, as an example, an application that renders a surface as an indexed triangle set with a texture mapped onto it. If the state related to this task is encapsulated in a single class, this class might look like the following:

class IndexedTriangleSet
	{
	...
	Vertex* vertices; // Array of vertices
	GLuint* indices; // Array of triangle vertex indices
	GLuint textureObjectId; // ID of the texture object holding the surface texture
	...
	void render(void); // Renders the triangle set
	};

The problem is that this class (and the application using it) would only work in a single-pipe environment. Depending on the architecture of the underlying system, it is not guaranteed that OpenGL objects (such as texture objects) will have the same IDs across different OpenGL contexts. In other words, if the above class is supposed to work in a multi-pipe environment, there are two approaches: (1) replicate entire IndexedTriangleSet objects for each context, or (2) store more than one texture object ID in each IndexedTriangleSet object. The first approach wastes resources because the vertex and triangle data live in the application's address space and can be shared between OpenGL contexts; the second approach is annoying because the programmer has to take care to allocate the proper number of object IDs, and ensure that the texture is uploaded into each OpenGL context separately. For a programmer who does not really anticipate ever using a multi-pipe system, both approaches are wasted effort, leading to many VR applications that will not run in multi-pipe VR environments.

The approach embodied by GLContextData/GLObject is to separate per-application state from per-context state, and to provide a mechanism to associate per-context state with an application when that state is needed for rendering. The IndexedTriangleSet class, reformulated to work with GLContextData, would look like the following:

class IndexedTriangleSet:public GLObject
	{
	...
	struct DataItem:public GLObject::DataItem // Structure containing per-context data
		{
		GLuint textureObjectId;
		
		DataItem(void); // Creates any per-context state
		virtual ~DataItem(void); // Destroys all per-context state
		};
	...
	Vertex* vertices; // Array of vertices
	GLuint* indices; // Array of triangle vertex indices
	...
	virtual void initContext(GLContextData& contextData) const; // Creates per-context data
	void render(GLContextData& contextData) const; // Renders the triangle set
	...
	};

After a class' state has been separated into per-application and per-context data, a developer does not have to know how many OpenGL contexts are used for rendering. The application will create one IndexedTriangleSet object, and initialize its per-application state. During rendering, that object's render() method will be called for every OpenGL context used by the application, each time using a different GLContextData object. The IndexedTriangleSet object will query its per-context state related to the current OpenGL context from the GLContextData object. In other words, the same IndexedTriangleSet object will see different per-context data, depending on which OpenGL context it is currently rendered in. This is also the reason why the render() method is declared const -- since the render() method will be called a number of times, it is not allowed to change per-application state from inside that method. Per-context state, however, can be changed -- that is why the GLContextData object passed into the render() method is not declared const.

One related problem with multi-pipe rendering is when to initialize and release per-context state. Since it is not allowed to change application state from inside a render() method, an application can only create new objects from some other method, for example an event callback. That means that per-context state must be initialized right before an object is rendered first in each context it is rendered in. Releasing per-context resources is an even bigger problem: Once an object has been deleted from somewhere outside the render() method, it is not available anymore to clean up after itself. The GLContextData method solves both these problems elegantly. Any class derived from GLObject contains a virtual method initContext(). This method is called right before the first time a new object is rendered in each OpenGL context. Inside of it, the application will typically create a new DataItem object, and store it in the passed GLContextData object (to later be retrieved in the render() method). If an object derived from GLObject is destroyed, the destructor of GLObject will ensure that any DataItem object belonging to it in any OpenGL context will be destroyed the next time that OpenGL context is made current for rendering. To ensure proper handling of per-context resources, the following procedure is required:

  1. Any class that has per-context state must be derived from GLObject.
  2. Any per-context state of the class must be separated into an embedded DataItem structure derived from GLObject::DataItem.
  3. The DataItem constructor allocates OpenGL resources (texture objects, vertex buffers, etc.), but does not necessarily have to initialize those resources. Example:
    IndexedTriangleSet::DataItem::DataItem(void)
    	:textureObjectId(0)
    	{
    	glGenTextures(1,&textureObjectId);
    	}
    
  4. The virtual DataItem destructor releases all allocated OpenGL resources. Example:
    IndexedTriangleSet::DataItem::~DataItem(void)
    	{
    	glDeleteTextures(1,&textureObjectId);
    	}
    
  5. The IndexedTriangleSet constructor creates per-application state, e.g., allocates and initializes the vertex and index arrays.
  6. The IndexedTriangleSet destructor destroys all per-application state.
  7. The IndexedTriangleSet initContext() method creates a data item, stores it in the GLContextData, and initializes the OpenGL resources. Example:
    void IndexedTriangleSet::initContext(GLContextData& contextData) const
    	{
    	/* Create a new data item: */
    	DataItem* dataItem=new DataItem();
    	
    	/* Associate object and data item in GLContextData: */
    	contextData.addDataItem(this,dataItem);
    	
    	/* Read and upload texture image into dataItem->textureObjectId: */
    	glBindTexture(GL_TEXTURE_2D,dataItem->textureObjectId);
    	...
    	
    	/* Protect texture object: */
    	glBindTexture(GL_TEXTURE_2D,0);
    	}
    
  8. The IndexedTriangleSet render() method retrieves the data item from the GLContextData, and uses it to render. Example:
    void IndexedTriangleSet::render(GLContextData& contextData) const
    	{
    	/* Retrieve data item from GLContextData: */
    	DataItem* dataItem=contextData.retrieveDataItem<DataItem>(this);
    	
    	/* Activate texture object: */
    	glBindTexture(GL_TEXTURE_2D,dataItem->textureObjectId);
    	
    	/* Render triangles: */
    	...
    	
    	/* Protect texture object: */
    	glBindTexture(GL_TEXTURE_2D,0);
    	}