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.
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 an unkown number of times, and possibly concurrently, 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.
IndexedTriangleSet::DataItem::DataItem(void) :textureObjectId(0) { glGenTextures(1,&textureObjectId); }
IndexedTriangleSet::DataItem::~DataItem(void) { glDeleteTextures(1,&textureObjectId); }
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); }
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); }
An example is an application that queries the availability of certain OpenGL extension in its initContext() method, and retains a "minimal set" of supported extensions in all used OpenGL contexts. It then checks for this minimal set during the first invocation of the frame() method, and changes its overall behavior accordingly, for example by preparing application-wide data structures needed to fall back to an alternative rendering method. It is important to remember that each object's initContext() method will be invoked an unknown number of times, once per OpenGL context, in no particular order and sometimes concurrently. Therefore, special care needs to be taken that any changes to per-application state from inside the initContext() method are reentrant and thread-safe (which is why the initContext method is declared const).
Here is an example of a Vrui application checking for the availability of a particular OpenGL extension in all used contexts:
class Test:public Vrui::Application,public GLObject { /* Embedded classes: */ private: struct DataItem:public GLObject::DataItem { ... }; /* Elements: */ mutable bool hasVertexBufferObject; // Flag whether all used OpenGL contexts support GL_ARB_vertex_buffer_object mutable Threads::Mutex counterMutex; // Mutex protecting the counter element mutable int counter; // Element counting how many contexts support the extension (example of non-trivial change) bool firstFrame; // Flag true on the first time frame() is invoked /* Constructors and destructors: */ public: Test(int& argc,char**& argv,char**& appDefaults); /* Methods: */ virtual void initContext(GLContextData& contextData) const; virtual void frame(void); virtual void display(GLContextData& contextData) const; }; Test::Test(int& argc,char**& argv,char**& appDefaults) :Vrui::Application(argc,argv,appDefaults), hasVertexBufferObject(true), // Is global "all" operation; has to be initialized to true counter(0), firstFrame(true) { /* Initialize application: */ ... } void Test::initContext(GLContextData& contextData) const { /* Create and register data item: */ ... /* Check for extension: */ if(GLARBVertexBufferObject::isSupported()) { /* Increase counter; requires locking due to possible race condition: */ { Threads::Mutex::Lock counterLock(counterMutex); ++counter; } } else { /* Set per-application variable to false; no locking required due to write-only access: */ hasVertexBufferObject=false; } /* Initialize per-context state: */ ... } void Test::frame(void) { /* Check if first frame: */ if(firstFrame) { /* Check if extension is supported: */ if(hasVertexBufferObject) { /* Do something: */ ... } else { /* Do something else: */ ... } /* Don't do this again: */ firstFrame=false; } /* Do per-frame operations: */ ... } void Test::display(GLContextData& contextData) const { /* Retrieve data item: */ ... /* Check for rendering path: */ if(hasVertexBufferObject) { /* Render one way: */ ... } else { /* Render another way: */ ... } }This scenario should be considered a special case. Normally, it is much more appropriate to handle decisions depending on capabilities of an OpenGL context, such as whether vertex buffer objects should be used, on a per-context basis from within the initContext() method and any rendering code. All state and initialization related to alternative rendering paths should be stored in the DataItem object, and paths should be selected on a per-context basis during display. The benefit is that applications running in a heterogeneous rendering environment (such as multiple graphics cards of different brands/models) could use the optimal rendering path for each card. In the IndexedTriangleSet example class introduced above, the embedded DataItem class could contain a flag whether its associated OpenGL context supports vertex buffer objects, and could use them whereever available. As a fallback, the class could render straight from the per-application vertex and index arrays.