GC & OpenGL (work in progress)

As discussed in the previous chapter, GC finalization occurs on the finalizer thread. This poses some problems on OpenGL resource deallocation, since the context used to create the resources is not available in the finalizer thread!

Since OpenGL functions cannot be called in finalizers, a different methodology must be followed. By implementing the disposable pattern, we can use the Dispose() method to deterministaclly destroy OpenGL resources in the main thread. By modifying the finalizer logic we can provide a way to flag resources as 'dead', and destroy them from the main thread. Last, by extending the concept of the OpenGL context, we can be notified of context destruction, to release all related resources.

The following code describes the implementation of the "OpenGL disposable pattern" in OpenTK, but it is easy to adapt this code to any managed OpenGL project:

// This code is out-of-date. Please do not use it!
 
// The OpenGL disposable pattern
class GraphicsResource: IDisposable
{
    int resource_handle;    // The OpenGL handle to the resource
    GraphicsContext context;      // The context which owns this resource
 
    public GraphicsResource()
    {
        // Obtain the current OpenGL context, and allocate the resource
        context = GraphicsContext.CurrentContext;
        if (context == null)
            throw new InvalidOperationException(String.Format(
                "No OpenGL context available in thread {0}.",
                System.Threading.Thread.CurrentThread.ManagedThreadId));
 
        resource_handle = [...];
 
        context.Destroy += ContextDisposed;
    }
 
    #region --- Disposable Pattern ---
 
    private void ContextDisposed(IGraphicsContext sender, EventArgs e)
    {
        context.Destroy -= ContextDisposed;
        // TODO: Shared resources shouldn't be destroyed here.
        Dispose();
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    // If the owning context is current then destroy the resource,
    // otherwise flag it (so it will be destroyed from the correct thread)..
    // TODO: Is the "manual" flag necessary? Simply checking for the
    // owning context should be enough.
    private void Dispose(bool manual)
    {
        if (!disposed)
        {
            if (!context.IsCurrent || !manual)
            {
                GC.KeepAlive(this);
                context.RegisterForDisposal(this);
            }
            else
            {
                // Destroy resource_handle through OpenGL
                disposed = true;
            }
        }
    }
 
    ~GraphicsResource()
    {
        Dispose(false);
    }
 
    #endregion
}

In OpenTK, each GraphicsContext class maintains a queue of OpenGL resources that need to be destroyed. Resources are added to this queue through the RegisterForDisposal() call, and they are destroyed through the DisposeResources() method. The whole process is deterministic: it is your responsibility to call DisposeResources at appropriate time intervals (or setup up a timer event to do this for you).

Resource creation takes a small performance hit due to the call to GraphicsContext.CurrentContext, while garbage collect-able OpenGL resources consume slightly more memory (due to the reference to the GraphicsContext). Prefer calling the Dispose() method to destroy resources instead of relying on the GC, as finalizable resources are only collected on a generation 1 or 2 GC sweep.

The current implementation in OpenTK does not take shared contexts into account - this will be taken care of in the near future.


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Hangar's picture

I see three glaring errors, and I dislike this implementation in general.

The three glaring errors:

1. An unsealed class should have a protected virtual Dispose(bool) method so that subclasses can be disposed as well.

2. GC.KeepAlive doesn't do what you probably think it does. It makes sure that the object is stays referenced until GC.KeepAlive is called. It does nothing if the object has already been marked for finalization. It also has no effect if the object is used later on, as it is in the next line when you get its member, context. Maybe you were thinking GC.ReRegisterForFinalize? But event that would not work as GC.SuppressFinalize is called later, and won't be called in case of exception.

3. The finalizer will not run when you expect it to. When you register for an event, the event holds a strong reference to the object. Since GC.SuppressFinalize is called whenever the event is fired, the only time that the finalizer will be called is if both the context and resource are collected at the same time, and the resource finalizer runs first. You can fix this with weak delegates, but I'm not sure you want to.

Here's how I might do it:

// The OpenGL disposable pattern
class GraphicsResource: IDisposable
{
    private int resource_handle;    // The OpenGL handle to the resource
    private GraphicsContext context;      // The context which owns this resource
    private bool disposed;        // Whether the resource has been disposed yet
 
    public GraphicsResource()
    {
        // Obtain the current OpenGL context, and allocate the resource
        context = GraphicsContext.CurrentContext;
        if (context == null)
            throw new InvalidOperationException(String.Format(
                "No OpenGL context available in thread {0}.",
                System.Threading.Thread.CurrentThread.ManagedThreadId));
 
        resource_handle = [...];
    }
 
    // Access to the resource handle
    public int ResourceHandle
    {
        get
        {
              if (disposed)
                   throw new ObjectDisposedException("GraphicsResource");
              return resource_handle;
        }
    }
 
    #region --- Disposable Pattern ---
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    // If the owning context is current then destroy the resource,
    // otherwise flag it (so it will be destroyed from the correct thread)..
    protected virtual void Dispose(bool manual)
    {
        if (!disposed)
        {
            if (!context.IsDestroyed)
            {
                if (context.IsCurrent)
                {
                    ReleaseResource(context,  resource_handle);
                }
                else
                {
                    GraphicsContext savedContext = context;
                    int savedResource = resource_handle;
                    context.RegisterForExecution(() => ReleaseResource(savedContext, savedResource));
                }
            }
            disposed = true;
        }
    }
 
    private static void ReleaseResource(GraphicsContext context, int  resource_handle)
    {
        // dispose of  resource_handle...
    }
 
    ~GraphicsResource()
    {
        Dispose(false);
    }
 
    #endregion
}

All access to the resource handle should throw an ObjectDisposedException if Dispose has been called. Consider it like an ArgumentException.

There's no need for the context to keep track of resources since you shouldn't be using a resource when the context is destroyed anyway. A context just needs a flag to mark itself as dead, IsDestroyed. The resource checks first whether the context is destroyed, and if so, releases or delegates the release of the unmanaged resource.

The lambda expression could instead just be a member function, or you could implement an interface and avoid the delegate, but Dispose should never silently fail to dispose the object. It can throw an exception to fail if you don't want to be able to dispose when the context is not current, but directly throwing an exception from a Dispose method is never a good idea, especially when the resource can be finalized.

The manual parameter to Dispose(bool) is never used since the only resource is unmanaged. However, it exists so that derived classes know why the resource is being destroyed.

Note that a concurrency issue is left over. Using and disposing a resource on different threads can cause a race condition, and for efficiency you may not want to fix that. However, GraphicsContext.RegisterForExecution must use locks if you expect it to run from multiple threads, and may have to be synchronized against IsDestroyed or IsCurrent, depending on how you handle it.

the Fiddler's picture

Thanks for the detailed explanation - good thing this code stayed hidden in the library after all.

3. Contexts aren't supposed to be finalized (the event is raised only when the context is explicitly disposed). If a context finalizer runs, it's too late - you will leak memory. This is because the context will (almost surely) be current on a different thread, causing MakeCurrent on the finalizer thread to fail. In that sense, it makes sense to keep the context alive until all its resources are destroyed.

There's no need for the context to keep track of resources since you shouldn't be using a resource when the context is destroyed anyway. A context just needs a flag to mark itself as dead, IsDestroyed. The resource checks first whether the context is destroyed, and if so, releases or delegates the release of the unmanaged resource.

Although the resource knows which context it was created on, it doesn't know whether it is shared in multiple contexts. Only the context itself can know whether it is safe to release a resource.

This means you'd probably have to mark shared resources with a reference count, but solving this problem is beyond my ken at the moment (*which* resources can be shared? who is going to hold the reference count, the resource or the context?)

Apart from this, your solution looks solid - I'll have to play with this when I find the time.

Hangar's picture

Note that I based that solution on the assumption that resources do not need to be destroyed before the context is--that memory leaks won't occur when you drop a context without telling it to delete a texture first. If I'm wrong about that, you have four options:

1. Disallow destructors on resources, and use something like what you had before.

2. Use weak delegates, a fairly complicated pattern, to implement the pattern you had before. The weak delegate pattern uses WeakReferences, so it could be inefficient.

3. Keep a weak collection of resources and dispose of them when the context is destroyed. This is similar to using weak delegates, but avoids use of delegates. (It still uses weak pointers.)

4. (The most complicated.) Separate out the concepts ResourceProxy, access to the resource; and ResourceHandle, the lifetime management. The context keeps track of all ResourceHandles, and destroys them upon context destruction if they aren't already destroyed. The proxy is what the user sees and works like the normal resource does now. The proxy's destructor and Dispose method clean up the ResourceHandle. Because the handle is separate from the proxy, the context can avoid weak references and still be collected. However, I'm not sure whether it's any faster because there are twice as many allocations and twice as many classes to write.

the Fiddler's picture

Note that I based that solution on the assumption that resources do not need to be destroyed before the context is--that memory leaks won't occur when you drop a context without telling it to delete a texture first.

In my experience, dropping a context without releasing its resources can be dangerous. Specifically, I've seen crashes when a VBO or shader is bound and the context is deleted.

#1 is not really desirable, as it effectively removes GL resources form the GC safety net (which is what this topic is all about).

#2 on its own is definitely not desirable, due to the above (collecting the context is dangerous without releasing its resources).

#3 is probably the most reasonable solution. It reverses the problem (now the context keeps track of resources, not vice versa), which makes it possible to drop the context cleanly (it simply destroys all registered resources). Weak references make resources GC-eligible, which is what we want. The only difficulty is that we need a way to signal the context about dead resources - that could be done with method #2, or by simply marking the resources as dead and having the context periodically scrub its table.

#4 I'm not sure if this is more complicated than #2 + #3, but I also don't think it's the place of OpenTK to dictate a specific usage pattern.

I like the idea of having an API applications can hook into, which allows to keep track of GL resources. Ideally this would be something as simple as implementing an IGraphicsResource interface and calling context.RegisterResources(resource).

However, if it isn't possible to implement this in an efficient or simple way, maybe it belongs to the user, not the library.

Hangar's picture

If it doesn't belong in the library, then you're effectively using #1.

#2 and #3 have similar results. The weak event pattern is a bit difficult to implement, but from the outside it's a little prettier: this link gives an overview of a bunch of different options. Interfaces will definitely be faster than delegates though.

And for #4: Both #2 and #3 dictate a usage pattern, the difference I see here is ease of understanding and implementation (complexity).

Looking at it from the complexity angle, stick with #3 or #1. I'm not sure on the exact figures, but I think WeakReference costs should be pretty low in this scenario. It doesn't cause an object to skip a generation like a finalizer, so I think it's worth the extra reliability.

the Fiddler's picture

Interfaces will definitely be faster than delegates though.
Since .Net 2.0, delegate calls cost the same as interface calls (on .Net 1.1 the first were significantly slower). The same should hold for recent mono versions, at least for non-generic delegates. The downside is that delegates consume memory, but they also are more flexible.

Unless weak references have hidden costs, they shouldn't affect performance in any significant way. According to #3, they will only be accessed twice - during resource creation and destruction.

DreaminD's picture

I've recently attempted to make my own implementation of resource-managing context on top of OpenTK, but currently this is impossible without modifying the source (DummyGLContext, IPlatformFactory and maybe some other names are not public).

I was trying to implement control over resource lifetime roughly this way:

  • Each Context keeps reference to reference-counted ResourceManager. When a shared context is created, it uses the same ResourceManager and increments its reference count; when context is disposed/finalized, reference count is decremented. When refcount is about to become zero, the last remaining context is used to delete all resources registered in ResourceManager (if it's not safe to make context current in the finalizer thread, resource disposal would be delegated to a suitable thread).
  • When resource is created, it registers itself in the ResourceManager of current context (ResourceManager keeps weak reference to the resource). While ResourceManager has not been destroyed, resource can be disposed
    • manually by user;
    • through becoming unreferenced (when finalized or when refcount reached zero).
  • Reference counting is supposed to be used the following way (e.g. for resource of type Texture):
    Reference<Texture> reference = new Reference<Texture>();
    reference.Target = some_texture; // increments refcount of some_texture
    ...
    // Now we will be using other_texture. We don't need to care whether some_texture is used by other objects or should be disposed.
    reference.Target = other_texture; // increments refcount of other_texture, decrements refcount of some_texture
  • Unreferenced (but not yet disposed) resources are added to ResourceManager's "disposal queue". To actually delete these queued resources, user would have to call context's Cleanup() (or similarly named) method whenever convenient.

Delaying resource disposal from finalizer would lead to object resurrection, but I suppose this is not inherently bad. There are also a lot of thread-safety (and maybe performance) issues to consider, but I'd prefer to discuss them when I have some working code to show :)