Commits

Michael Ludwig  committed 789e055

Simplify the implementation of ResourceManager and ContextManager to use a single thread (with significantly reducted threading handling then), remove the need for a super-ResourceHandle type, and repair AbstractFramework and AbstractSurfaces to work with these new changes.

  • Participants
  • Parent commits 801ee1f

Comments (0)

Files changed (15)

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/AbstractFixedFunctionRenderer.java

         public CombineFunction rgbFunc = CombineFunction.MODULATE;
         public CombineFunction alphaFunc = CombineFunction.MODULATE;
         
-        public final CombineOp[] opRgb = {CombineOp.COLOR, CombineOp.COLOR, CombineOp.ALPHA};
-        public final CombineOp[] opAlpha = {CombineOp.ALPHA, CombineOp.ALPHA, CombineOp.ALPHA};
+        public final CombineOperand[] opRgb = {CombineOperand.COLOR, CombineOperand.COLOR, CombineOperand.ALPHA};
+        public final CombineOperand[] opAlpha = {CombineOperand.ALPHA, CombineOperand.ALPHA, CombineOperand.ALPHA};
         
         public final CombineSource[] srcRgb = {CombineSource.CURR_TEX, CombineSource.PREV_TEX, CombineSource.CONST_COLOR};
         public final CombineSource[] srcAlpha = {CombineSource.CURR_TEX, CombineSource.PREV_TEX, CombineSource.CONST_COLOR};
             setTextureMode(i, EnvMode.MODULATE);
             
             setTextureCombineFunction(i, CombineFunction.MODULATE, CombineFunction.MODULATE);
-            setTextureCombineOpAlpha(i, 0, CombineSource.CURR_TEX, CombineOp.ALPHA);
-            setTextureCombineOpAlpha(i, 1, CombineSource.PREV_TEX, CombineOp.ALPHA);
-            setTextureCombineOpAlpha(i, 2, CombineSource.CONST_COLOR, CombineOp.ALPHA);
-            setTextureCombineOpRgb(i, 0, CombineSource.CURR_TEX, CombineOp.COLOR);
-            setTextureCombineOpRgb(i, 1, CombineSource.PREV_TEX, CombineOp.COLOR);
-            setTextureCombineOpRgb(i, 2, CombineSource.CONST_COLOR, CombineOp.ALPHA);
+            setTextureCombineOpAlpha(i, 0, CombineSource.CURR_TEX, CombineOperand.ALPHA);
+            setTextureCombineOpAlpha(i, 1, CombineSource.PREV_TEX, CombineOperand.ALPHA);
+            setTextureCombineOpAlpha(i, 2, CombineSource.CONST_COLOR, CombineOperand.ALPHA);
+            setTextureCombineOpRgb(i, 0, CombineSource.CURR_TEX, CombineOperand.COLOR);
+            setTextureCombineOpRgb(i, 1, CombineSource.PREV_TEX, CombineOperand.COLOR);
+            setTextureCombineOpRgb(i, 2, CombineSource.CONST_COLOR, CombineOperand.ALPHA);
 
             setTextureCoordGeneration(i, TexCoordSource.ATTRIBUTE);
             
     protected abstract void glCombineFunction(CombineFunction func, boolean rgb);
     
     @Override
-    public void setTextureCombineOpAlpha(int tex, int operand, CombineSource src, CombineOp op) {
+    public void setTextureCombineOpAlpha(int tex, int operand, CombineSource src, CombineOperand op) {
         if (src == null || op == null)
-            throw new NullPointerException("CombineSource and CombineOp can't be null");
+            throw new NullPointerException("CombineSource and CombineOperand can't be null");
         if (operand < 0 || operand > 2)
             throw new IllegalArgumentException("Operand must be 0, 1, or 2");
-        if (op == CombineOp.COLOR || op == CombineOp.ONE_MINUS_COLOR)
-            throw new IllegalArgumentException("Illegal CombineOp for alpha: " + op);
+        if (op == CombineOperand.COLOR || op == CombineOperand.ONE_MINUS_COLOR)
+            throw new IllegalArgumentException("Illegal CombineOperand for alpha: " + op);
         if (tex < 0)
             throw new IllegalArgumentException("Texture unit must be at least 0, not: " + tex);
         if (tex >= textures.length)
     /**
      * Invoke OpenGL calls to set the combine op
      */
-    protected abstract void glCombineOp(int operand, CombineOp op, boolean rgb);
+    protected abstract void glCombineOp(int operand, CombineOperand op, boolean rgb);
     
     @Override
-    public void setTextureCombineOpRgb(int tex, int operand, CombineSource src, CombineOp op) {
+    public void setTextureCombineOpRgb(int tex, int operand, CombineSource src, CombineOperand op) {
         if (src == null || op == null)
-            throw new NullPointerException("CombineSource and CombineOp can't be null");
+            throw new NullPointerException("CombineSource and CombineOperand can't be null");
         if (operand < 0 || operand > 2)
             throw new IllegalArgumentException("Operand must be 0, 1, or 2");
         if (tex < 0)

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/AbstractFramework.java

 package com.ferox.renderer.impl;
 
-import java.util.Comparator;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import com.ferox.input.EventQueue;
 import com.ferox.renderer.DisplayMode;
 import com.ferox.renderer.Framework;
+import com.ferox.renderer.FrameworkException;
 import com.ferox.renderer.OnscreenSurface;
 import com.ferox.renderer.OnscreenSurfaceOptions;
 import com.ferox.renderer.RenderCapabilities;
 import com.ferox.renderer.Task;
 import com.ferox.renderer.TextureSurface;
 import com.ferox.renderer.TextureSurfaceOptions;
-import com.ferox.resource.GlslShader;
 import com.ferox.resource.Resource;
 import com.ferox.resource.Resource.Status;
 
     private final CopyOnWriteArraySet<AbstractSurface> surfaces;
     
     private final SurfaceFactory surfaceFactory;
-    private final OpenGLContext sharedContext;
+    
     private RenderCapabilities renderCaps; // final after initialize() has been called.
     
     private final LifeCycleManager lifecycleManager;
      *            low-level graphics work for resources
      * @throws NullPointerException if surfaceFactory or any driver is null
      */
-    public AbstractFramework(SurfaceFactory surfaceFactory, int numThreads, ResourceDriver<?>... drivers) {
+    public AbstractFramework(SurfaceFactory surfaceFactory, int numThreads, ResourceDriver... drivers) {
         if (surfaceFactory == null)
             throw new NullPointerException("SurfaceFactory cannot be null");
         
         this.surfaceFactory = surfaceFactory;
-        
-        // Create the shared context now for three reasons:
-        //  1. It's nice to let it be final
-        //  2. If creation fails, construction fails. If it succeeds the hardware probably works well enough
-        //     that we won't break later on
-        //  3. We can construct the context manager here, too
-        sharedContext = surfaceFactory.createShadowContext(null);
-        contextManager = new ContextManager(surfaceFactory, sharedContext, numThreads);
-        resourceManager = new ResourceManager(new GlslCompatibleLockComparator(), contextManager, drivers);
+
+        contextManager = new ContextManager();
+        resourceManager = new ResourceManager(contextManager, drivers);
         
         surfaces = new CopyOnWriteArraySet<AbstractSurface>();
-        lifecycleManager = new LifeCycleManager("Ferox Framework");
+        lifecycleManager = new LifeCycleManager("ferox-renderer");
         
         eventQueue = new EventQueue();
         
             @Override
             public void run() {
                 // Start up the context manager and resource manager
-                contextManager.initialize(lifecycleManager);
+                contextManager.initialize(lifecycleManager, surfaceFactory);
                 resourceManager.initialize(lifecycleManager);
                 
                 // Fetch the RenderCapabilities now, we do it this way to improve
                 // the Framework creation time instead of forcing OpenGL wrappers to 
                 // create and discard a context solely for capabilities detection.
-                Future<RenderCapabilities> caps = contextManager.queue(new Callable<RenderCapabilities>() {
+                Future<RenderCapabilities> caps = contextManager.invokeOnContextThread(new Callable<RenderCapabilities>() {
                     @Override
                     public RenderCapabilities call() throws Exception {
                         OpenGLContext context = contextManager.ensureContext();
                         return context.getRenderCapabilities();
                     }
-                }, DEFAULT_RESOURCE_TASK_GROUP);
+                }, false);
                 
                 renderCaps = getFuture(caps);
             }
     }
     
     @Override
-    public OnscreenSurface createSurface(OnscreenSurfaceOptions options) {
-        lifecycleManager.getLock().lock();
-        try {
-            if (lifecycleManager.getStatus() == LifeCycleManager.Status.ACTIVE) {
+    public OnscreenSurface createSurface(final OnscreenSurfaceOptions options) {
+        if (options == null)
+            throw new NullPointerException("Options cannot be null");
+        
+        // This task is not accepted during shutdown
+        Future<OnscreenSurface> create = contextManager.invokeOnContextThread(new Callable<OnscreenSurface>() {
+            @Override
+            public OnscreenSurface call() throws Exception {
                 synchronized(fullscreenLock) {
                     if (options.getFullscreenMode() != null && fullscreenSurface != null)
                         throw new SurfaceCreationException("Cannot create fullscreen surface when an existing surface is fullscreen");
                     
-                    AbstractOnscreenSurface created = surfaceFactory.createOnscreenSurface(this, options, sharedContext);
+                    AbstractOnscreenSurface created = surfaceFactory.createOnscreenSurface(AbstractFramework.this, options, 
+                                                                                           contextManager.getSharedContext());
                     surfaces.add(created);
                     
                     if (created.getOptions().getFullscreenMode() != null)
                         fullscreenSurface = created;
                     return created;
                 }
-            } else
-                return null;
-        } finally {
-            lifecycleManager.getLock().unlock();
-        }
+            }
+        }, false);
+        return getFuture(create);
     }
 
     @Override
-    public TextureSurface createSurface(TextureSurfaceOptions options) {
-        lifecycleManager.getLock().lock();
-        try {
-            if (lifecycleManager.getStatus() == LifeCycleManager.Status.ACTIVE) {
-                AbstractTextureSurface created = surfaceFactory.createTextureSurface(this, options, sharedContext);
+    public TextureSurface createSurface(final TextureSurfaceOptions options) {
+        if (options == null)
+            throw new NullPointerException("Options cannot be null");
+        
+        // This task is not accepted during shutdown
+        Future<TextureSurface> create = contextManager.invokeOnContextThread(new Callable<TextureSurface>() {
+            @Override
+            public TextureSurface call() throws Exception {
+                AbstractTextureSurface created = surfaceFactory.createTextureSurface(AbstractFramework.this, options, 
+                                                                                     contextManager.getSharedContext());
                 
                 // Mark all textures as non-disposable
                 if (created.getDepthBuffer() != null)
                 
                 surfaces.add(created);
                 return created;
-            } else
-                return null;
-        } finally {
-            lifecycleManager.getLock().unlock();
-        }
+            }
+        }, false);
+        
+        return getFuture(create);
     }
 
     @Override
     public void destroy() {
+        final List<Exception> surfaceDestroyExceptions = new ArrayList<Exception>();
         lifecycleManager.stop(new Runnable() {
             @Override
             public void run() {
-                // This is only run after the manager has been stopped, and all
-                // context threads are ended, so the surfaces array will not be
-                // modified at this point
-                for (AbstractSurface surface: surfaces) {
-                    surface.destroy();
-                }
-                surfaces.clear();
-                
-                // Destroy the shared context
-                sharedContext.destroy();
-                
                 // Shutdown the event queue
                 eventQueue.shutdown();
+                
+                // Destroy all remaining surfaces
+                // The loop is structured this way so that we don't get an
+                // iterator snapshot that's not updated if there were any
+                // pending creates before we transitioned to STOPPING
+                while(!surfaces.isEmpty()) {
+                    AbstractSurface toDestroy = surfaces.iterator().next();
+                    try {
+                        toDestroy.destroy().get();
+                    } catch(Exception e) {
+                        // accumulate the exceptions but continue to destroy
+                        // all of the surfaces
+                        surfaceDestroyExceptions.add(e);
+                    }
+                }
             }
         });
+        
+        if (!surfaceDestroyExceptions.isEmpty()) {
+            throw new FrameworkException(surfaceDestroyExceptions.size() + " exception(s) while destroying surface, first failure:", 
+                                         surfaceDestroyExceptions.get(0));
+        }
     }
 
     @Override
     }
 
     @Override
-    public <T> Future<T> queue(Task<T> task, String group) {
-        return contextManager.queue(new TaskCallable<T>(task), group);
+    public <T> Future<T> queue(Task<T> task) {
+        // Specify false so that these tasks are only queued while the
+        // state is ACTIVE
+        return contextManager.invokeOnContextThread(new TaskCallable<T>(task), false);
     }
 
     @Override
     public Status update(Resource resource) {
-        return getFuture(queue(new UpdateResourceTask(resource), DEFAULT_RESOURCE_TASK_GROUP));
+        return getFuture(queue(new UpdateResourceTask(resource)));
     }
 
     @Override
     public void dispose(Resource resource) {
-        getFuture(queue(new DisposeResourceTask(resource), DEFAULT_RESOURCE_TASK_GROUP));
+        getFuture(queue(new DisposeResourceTask(resource)));
     }
 
     @Override
-    public void flush(Surface surface, String group) {
-        getFuture(queue(new FlushSurfaceTask(surface), group));
+    public void flush(Surface surface) {
+        getFuture(queue(new FlushSurfaceTask(surface)));
     }
 
     @Override
-    public void sync(String group) {
-        getFuture(queue(new EmptyTask(), group));
+    public void sync() {
+        getFuture(queue(new EmptyTask()));
     }
     
     private <T> T getFuture(Future<T> future) {
             throw new RuntimeException(e);
         } catch (ExecutionException e) {
             throw new RuntimeException(e);
+        } catch (CancellationException e) {
+            // bury the cancel request so that the help methods don't 
+            // throw exceptions when the framework is being destroyed
+            return null;
         }
     }
 
             return task.run(new HardwareAccessLayerImpl(AbstractFramework.this));
         }
     }
-    
-    /*
-     * Implement a Comparator<Resource> that pushes all GlslShaders to the end of the locking,
-     * so that the AbstractGlslRenderer works correctly. This isn't a perfect solution but its
-     * the only way to make the renderer safe in the event of bad resource updates.
-     */
-    private static class GlslCompatibleLockComparator implements Comparator<Resource> {
-        @Override
-        public int compare(Resource o1, Resource o2) {
-            boolean glsl1 = o1 instanceof GlslShader;
-            boolean glsl2 = o2 instanceof GlslShader;
-            
-            if ((glsl1 && glsl2) || (!glsl1 && !glsl2)) {
-                // if both are GlslShaders or both aren't GlslShaders, order by resource id
-                return o1.getId() - o2.getId();
-            } else if (glsl1 && !glsl2) {
-                // o1 is a GlslShader, so push it to the end
-                return 1;
-            } else {
-                // o2 is a GlslShader, so push it to the end
-                return -1;
-            }
-        }
-    }
 }

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/AbstractRenderer.java

     }
 
     @Override
-    public void setStencilUpdateOps(StencilOp stencilFail, StencilOp depthFail, StencilOp depthPass) {
+    public void setStencilUpdateOps(StencilUpdate stencilFail, StencilUpdate depthFail, StencilUpdate depthPass) {
         delegate.setStencilUpdateOps(stencilFail, depthFail, depthPass);
     }
 
     @Override
-    public void setStencilUpdateOpsBack(StencilOp stencilFail, StencilOp depthFail, StencilOp depthPass) {
+    public void setStencilUpdateOpsBack(StencilUpdate stencilFail, StencilUpdate depthFail, StencilUpdate depthPass) {
         delegate.setStencilUpdateOpsBack(stencilFail, depthFail, depthPass);
     }
 
     @Override
-    public void setStencilUpdateOpsFront(StencilOp stencilFail, StencilOp depthFail, StencilOp depthPass) {
+    public void setStencilUpdateOpsFront(StencilUpdate stencilFail, StencilUpdate depthFail, StencilUpdate depthPass) {
         delegate.setStencilUpdateOpsFront(stencilFail, depthFail, depthPass);
     }
 

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/AbstractSurface.java

 package com.ferox.renderer.impl;
 
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.ReentrantLock;
 
             ((AbstractRenderer) glsl).activate(this, context, framework.getResourceManager());
     }
 
-    // FIXME: maybe add prev/next surfaces to these so that surfaces like fbo surface
-    // can choose what to do?
-    
     /**
      * onSurfaceDeactivate() is a listener method that is invoked by
      * ContextManager when a surface is deactivated. The provided context is the
     }
     
     @Override
-    public void destroy() {
+    public Future<Void> destroy() {
+        // First call to destroy handles the destroy operation
         if (destroyed.compareAndSet(false, true)) {
-            // Since this is just a remove operation, we don't need to wait for
-            // the lock on the surface.
-            framework.markSurfaceDestroyed(this);
-            
-            // Check if we already have a lock
-            if (lock.isHeldByCurrentThread() && framework.getContextManager().isContextThread()) {
-                // If we're locked on a context thread then we need to forcefully unlock
-                // it first so that it gets deactivated and its context released
-                framework.getContextManager().forceRelease(this);
-            }
-            
-            // Use the ContextManager method to handle negotiating across threads with
-            // persistent locks.
-            framework.getContextManager().lock(this);
-            try {
-                destroyImpl();
-            } finally {
-                // ContextManager.lock() doesn't create a persistent lock so
-                // we can use unlock directly on our lock object.
-                lock.unlock();
-            }
+            // Accept this even during shutdown so that surfaces are destroyed
+            return framework.getContextManager().invokeOnContextThread(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    // Must force a release in case this surface was a context provider
+                    framework.getContextManager().forceRelease(AbstractSurface.this);
+                    destroyImpl();
+                    return null;
+                }
+            }, true);
+        } else {
+            // If we've already been destroyed, use a completed future so
+            // it's seen as completed
+            return new CompletedFuture<Void>(null);
         }
     }
     

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/AbstractTextureSurface.java

 import com.ferox.renderer.SurfaceCreationException;
 import com.ferox.renderer.TextureSurface;
 import com.ferox.renderer.TextureSurfaceOptions;
-import com.ferox.renderer.impl.ResourceManager.LockToken;
 import com.ferox.resource.BufferData.DataType;
 import com.ferox.resource.Mipmap;
 import com.ferox.resource.Resource.Status;
     
     private final TextureSurfaceOptions options;
     
-    private final LockToken<? extends Texture>[] colorLocks;
-    private LockToken<? extends Texture> depthLock;
+    private final boolean[] colorLocks;
+    private boolean depthLock;
     
-    @SuppressWarnings("unchecked")
     public AbstractTextureSurface(AbstractFramework framework, TextureSurfaceOptions options) {
         super(framework);
         if (options == null)
         activeLayer = options.getActiveLayer();
         activeDepth = options.getActiveDepthPlane();
         
-        colorLocks = new LockToken[colorTextures.length];
+        colorLocks = new boolean[colorTextures.length];
+        depthLock = false;
         
         updateTextures(colorTextures, depthTexture, framework);
     }
     
-    protected ResourceHandle getDepthHandle() {
-        return depthLock.getResourceHandle();
-    }
-    
-    protected ResourceHandle getColorHandle(int buffer) {
-        return colorLocks[buffer].getResourceHandle();
-    }
-    
     @Override
     public void onSurfaceActivate(OpenGLContext context, int activeLayer) {
         super.onSurfaceActivate(context, activeLayer);
         // mutated/read by another surface when being rendered into
         ResourceManager manager = getFramework().getResourceManager();
         for (int i = 0; i < colorLocks.length; i++) {
-            colorLocks[i] = manager.acquireFullLock(context, colorTextures[i]);
+            colorLocks[i] = manager.lockExclusively(colorTextures[i]);
         }
         
         if (depthTexture != null)
-            depthLock = manager.acquireFullLock(context, depthTexture);
+            depthLock = manager.lockExclusively(depthTexture);
         else
-            depthLock = null;
+            depthLock = false;
     }
     
     @Override
         
         // unlock all held locks in reverse order they were acquired
         ResourceManager manager = getFramework().getResourceManager();
-        if (depthLock != null) {
-            manager.unlock(depthLock);
-            depthLock = null;
+        if (depthLock) {
+            manager.unlockExclusively(depthTexture);
+            depthLock = false;
         }
         
         for (int i = colorLocks.length - 1; i >= 0; i--) {
-            manager.unlock(colorLocks[i]);
-            colorLocks[i] = null;
+            if (colorLocks[i]) {
+                manager.unlockExclusively(colorTextures[i]);
+                colorLocks[i] = false;
+            }
         }
     }
     

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/BufferUtil.java

  * @author Michael Ludwig
  */
 public class BufferUtil {
-    public static final int SIZEOF_FLOAT = 4;
-    public static final int SIZEOF_INT = 4;
-    public static final int SIZEOF_SHORT = 2;
-
     /**
      * Create a new FloatBuffer of the given capacity.
      * 
      * @return A new direct FloatBuffer
      */
     public static FloatBuffer newFloatBuffer(int size) {
-        return newByteBuffer(size * SIZEOF_FLOAT).asFloatBuffer();
+        return newByteBuffer(size * DataType.FLOAT.getByteCount()).asFloatBuffer();
     }
     
     /**
      * @return A new direct IntBuffer
      */
     public static IntBuffer newIntBuffer(int size) {
-        return newByteBuffer(size * SIZEOF_INT).asIntBuffer();
+        return newByteBuffer(size * DataType.UNSIGNED_INT.getByteCount()).asIntBuffer();
     }
     
     /**
      * @return A new direct ShortBuffer
      */
     public static ShortBuffer newShortBuffer(int size) {
-        return newByteBuffer(size * SIZEOF_SHORT).asShortBuffer();
+        return newByteBuffer(size * DataType.UNSIGNED_SHORT.getByteCount()).asShortBuffer();
     }
     
     /**

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/CompletedFuture.java

+package com.ferox.renderer.impl;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Future that wraps an already prepared value, so the task the Future
+ * represents has completed.
+ * 
+ * @author Michael Ludwig
+ * @param <T>
+ */
+public class CompletedFuture<T> implements Future<T> {
+    private final T value;
+    
+    public CompletedFuture(T value) {
+        this.value = value;
+    }
+    
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return false;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    @Override
+    public boolean isDone() {
+        return true;
+    }
+
+    @Override
+    public T get() throws InterruptedException, ExecutionException {
+        return value;
+    }
+
+    @Override
+    public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        return value;
+    }
+}

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/ContextManager.java

 
 import java.util.concurrent.BlockingDeque;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
 
-import com.ferox.renderer.Framework;
+import com.ferox.renderer.FrameworkException;
 import com.ferox.renderer.impl.LifeCycleManager.Status;
 
 /**
  * <p>
- * ContextManager is the manager that handles the internal threads that run code
- * within a valid OpenGL context. It has logic to keep a context current after
- * task has completed, meaning that it may not need to have expensive
+ * ContextManager is the manager that handles the internal threads that runs
+ * code within a valid OpenGL context. It has logic to keep a context current
+ * after task has completed, meaning that it does not need to have expensive
  * makeCurrent()/release() cycles in the common case where a single surface is
  * rendered into repeatedly.
- * </p>
  * <p>
  * A newly constructed ContextManager is not ready to use until its
  * {@link #initialize(LifeCycleManager)} is called. The ContextManager is
  * expected to live within the life cycle of its owning Framework (as enforced
  * by the LifeCycleManager).
- * </p>
  * 
  * @author Michael Ludwig
  */
 public class ContextManager {
-    private LifeCycleManager lifecycleManager; // "final" after initialize() is called
-    
-    private final SurfaceFactory surfaceFactory;
-    private final OpenGLContext sharedContext;
-    
-    private final ConcurrentMap<String, ContextThread> groupAffinity; // A group will be placed in this map at most once
-    private final ConcurrentMap<AbstractSurface, ContextThread> persistentSurfaceLocks;
-    
-    private final ContextThread[] threads;
-    private final AtomicInteger nextThread;
+    // "final" after initialize() is called
+    private LifeCycleManager lifecycleManager; 
+    private ContextThread thread;
+    private OpenGLContext sharedContext;
 
     /**
      * <p>
-     * Create a new ContextManager that uses the given SurfaceFactory to create
-     * offscreen contexts to use when a Surface's context is not available for
-     * OpenGL work. <tt>sharedContext</tt> must be the Framework's shared
-     * context that enables resource sharing across all created contexts. If
-     * <tt>numThreads</tt> is less than or equal to 0, the number of created
-     * threadsd will be equal to the number of available processors.
-     * </p>
-     * <p>
-     * The created ContextManager cannot be used until it has been
-     * {@link #initialize(LifeCycleManager) initialized}.
-     * </p>
-     * 
-     * @see #initialize(LifeCycleManager)
-     * @param contextFactory The SurfaceFactory
-     * @param sharedContext The OpenGL context that facilitates resource sharing
-     * @param numThreads The number of internal threads to create for running
-     *            queued tasks
-     * @throws NullPointerException if contextFactory or sharedContext are null
-     */
-    public ContextManager(SurfaceFactory contextFactory, OpenGLContext sharedContext, int numThreads) {
-        if (contextFactory == null)
-            throw new NullPointerException("SurfaceFactory cannot be null");
-        if (sharedContext == null)
-            throw new NullPointerException("Shared context cannot be null");
-        
-        if (numThreads <= 0)
-            numThreads = Runtime.getRuntime().availableProcessors();
-        
-        this.sharedContext = sharedContext;
-        surfaceFactory = contextFactory;
-        
-        groupAffinity = new ConcurrentHashMap<String, ContextManager.ContextThread>();
-        persistentSurfaceLocks = new ConcurrentHashMap<AbstractSurface, ContextManager.ContextThread>();
-        
-        threads = new ContextThread[numThreads];
-        nextThread = new AtomicInteger(0);
-    }
-
-    /**
-     * <p>
-     * Complete the initialization of this ContextManager and start up all
-     * required internal threads that process all queued tasks. This method ties
-     * the ContextManager to the life cycle enforced by the given
-     * LifeCycleManager. It is required that this method is called by the
-     * ContextManager's owner in the initialization Runnable passed to
+     * Complete the initialization of this ContextManager and start up the
+     * thread used to process GPU-based tasks. It will also create the shared
+     * context used by every surface with this manager.. This method ties the
+     * ContextManager to the life cycle enforced by the given LifeCycleManager.
+     * It is required that this method is called by the ContextManager's owner
+     * in the initialization Runnable passed to
      * {@link LifeCycleManager#start(Runnable)}.
-     * </p>
      * <p>
      * The ContextManager will automatically terminate its threads when it
      * detects that the LifeCycleManager is being shutdown. All internal threads
      * are managed threads so the final destruction code passed to
      * {@link LifeCycleManager#destroy(Runnable)} will not run until the
      * ContextManager stops processing tasks.
-     * </p>
      * <p>
      * The ContextManager cannot be initialized more than once. It is illegal to
      * use a LifeCycleManager that has a status other than STARTING (i.e. within
      * the scope of its initialize() method).
-     * </p>
      * 
      * @param lifecycle The LifeCycleManager that controls when the
      *            ContextManager ends
-     * @throws NullPointerException if lifecycle is null
+     * @param surfaceFactory The SurfaceFactory to create the shared context
+     *            with
+     * @throws NullPointerException if lifecycle or surfaceFactory are null
      * @throws IllegalStateException if lifecycle doesn't have a status of
      *             STARTING, or if the ContextManager has already been
      *             initialized
      */
-    public void initialize(LifeCycleManager lifecycle) {
+    public void initialize(LifeCycleManager lifecycle, SurfaceFactory surfaceFactory) {
         if (lifecycle == null)
             throw new NullPointerException("LifeCycleManager cannot be null");
+        if (surfaceFactory == null)
+            throw new NullPointerException("SurfaceFactory cannot be null");
         
         // We are assuming that we're in the right threading situation, so this is safe.
         // If this is called outside of the manager's lock then all bets are off, but that's their fault.
             lifecycleManager = lifecycle;
         }
         
-        // Must use thread group that is a child of the managed group
-        ThreadGroup group = new ThreadGroup(lifecycle.getManagedThreadGroup(), "ContextManager Tasks");
-        for (int i = 0; i < threads.length; i++) {
-            String name = "task-thread-" + i;
-            if (i == 0) {
-                // First task thread will use the shared context as its shadow context
-                threads[i] = new ContextThread(group, name, sharedContext);
-            } else {
-                // No shadow context until its needed
-                threads[i] = new ContextThread(group, name);
-            }
-            
-            // Must start a managed thread, but we can't use the convenience method
-            // because the ContextManager uses a special subclass of Thread
-            lifecycle.startManagedThread(threads[i]);
+        thread = new ContextThread(lifecycle.getManagedThreadGroup(), "gpu-task-thread");
+
+        // Must start a managed thread, but we can't use the convenience method
+        // because the ContextManager uses a special subclass of Thread
+        lifecycle.startManagedThread(thread);
+        
+        // Very first task must be to allocate the shared context
+        try {
+            invokeOnContextThread(new ConstructContextCallable(surfaceFactory), false).get();
+        } catch (InterruptedException e) {
+            // ignore for now
+        } catch (ExecutionException e) {
+            throw new FrameworkException("Error creating shared context", e.getCause());
         }
     }
+    
+    /**
+     * @return The shared context that must be used by all surfaces for this
+     *         manager
+     * @throws IllegalStateException if the context manager hasn't been properly
+     *             initialized yet
+     */
+    public OpenGLContext getSharedContext() {
+        if (sharedContext == null)
+            throw new IllegalStateException("Shared context has not been created yet");
+        return sharedContext;
+    }
 
     /**
      * <p>
-     * Queue the given Callable task to one of the inner threads managed by this
-     * ContextManager. The provided group has the same meaning and implications
-     * as in {@link Framework#queue(com.ferox.renderer.Task, String)}. The
-     * ContextManager is allowed to run tasks with the same group on different
-     * threads. The only restriction is that tasks in the same group run in the
-     * chronological order they were queued (which can be non-deterministic if
-     * multiple threads queue to the same group).
-     * </p>
+     * Invoke the given Callable on the context thread managed by this manager.
+     * If the calling thread is not the context thread, this task is queued
+     * behind any other pending tasks. However, if the calling thread is the
+     * context thread, this will run the task immediately. In this case the
+     * returned Future will have already completed.
      * <p>
      * If the LifeCycleManager controlling this ContextManager is being
      * shutdown, or has been shutdown, the returned Future is not queued and is
      * preemptively cancelled. It will never be null.
-     * </p>
      * 
      * @param <T> The type of data returned by task
      * @param task The task to run on an internal thread
-     * @param group The group that controls execution flow
+     * @param acceptOnShutdown True if the task should be queued even while
+     *            shutting down
      * @return A Future linked to the queued task, will be cancelled if the
      *         ContextManager has been shutdown or is shutting down
-     * @throws NullPointerException if task or group are null
+     * @throws NullPointerException if task is null
      */
-    public <T> Future<T> queue(Callable<T> task, String group) {
+    public <T> Future<T> invokeOnContextThread(Callable<T> task, boolean acceptOnShutdown) {
         if (task == null)
             throw new NullPointerException("Task cannot be null");
-        if (group == null)
-            throw new NullPointerException("A null group is not allowed");
-        
-        // FIXME: this thread distribution logic to preserve within-group ordering
-        // works but is naive. ideally we should have something that can switch
-        // across threads if there is no other task in a group taking precedent.
-        // - the current approach could accidentally put all groups onto a single thread permanently
-        //   if things are queued in a bad order
-        ContextThread thread = groupAffinity.get(group);
-        if (thread == null) {
-            // must assign the group to a new thread, using a round-robin policy through the threads
-            thread = threads[nextThread.getAndIncrement() % threads.length];
-            ContextThread oldThread = groupAffinity.putIfAbsent(group, thread);
-            if (oldThread != null) {
-                // Another thread tried to queue for the same group, 
-                // so use that thread instead (this means that nextThread was
-                // advanced an extra count, but that shouldn't be too bad).
-                thread = oldThread;
-            }
-        }
-        
-        return queue(new Sync<T>(task), thread, false);
-    }
-
-    /**
-     * Return whether or not the current Thread is a thread managed by this
-     * ContextManager and is capable of having OpenGL contexts current on it.
-     * 
-     * @return True if the calling Thread is an inner thread managed by this
-     *         ContextManager
-     */
-    public boolean isContextThread() {
-        return Thread.currentThread() instanceof ContextThread;
-    }
-
-    /**
-     * Unlock the given surface from the current Thread. If the surface is not
-     * locked on this thread, no action is performed. This can only be called
-     * while on a context thread. If the Surface is currently active, it will be
-     * deactivated. If the Surface has its context in use, the context will be
-     * released. This may cause a second surface to be deactivated in the
-     * process (i.e. an FBO surface piggybacking on an OnscreenSurface that is
-     * being released).
-     * 
-     * @param surface The surface to unlock
-     * @throws NullPointerException if the surface is null
-     * @throws IllegalStateException if {@link #isContextThread()} returns false
-     */
-    public void forceRelease(AbstractSurface surface) {
-        if (surface == null)
-            throw new NullPointerException("Surface cannot be null");
-        Thread current = Thread.currentThread();
-        if (current instanceof ContextThread) {
-            // Delegate to the thread implementation
-            ((ContextThread) current).unlock(surface);
-        } else {
-            // Must only call on the ContextThread
-            throw new IllegalStateException("Cannot call unlock() on this thread");
-        }
-    }
-
-    /**
-     * <p>
-     * Acquire the exclusive lock to the given AbstractSurface for the current
-     * thread. This should be used instead of simply
-     * <code>surface.getLock().lock()</code> because the ContextManager's
-     * threads can hold a surface's lock across multiple frames to avoid the
-     * cost of constantly switching OpenGL contexts.
-     * </p>
-     * <p>
-     * This will negotiate the unlocking of a surface with any internal threads
-     * that acquire the lock first. The owning thread is allowed to finish its
-     * currently running task before it unlocks the surface.
-     * </p>
-     * 
-     * @param surface The surface to lock
-     * @throws NullPointerException if surface is null
-     */
-    public void lock(AbstractSurface surface) {
-        if (surface == null)
-            throw new NullPointerException("Surface cannot be null");
-        
-        // This method is the trickiest part of the lock management used in ContextManager.
-        // Because each thread will hold onto a context-providing surface after a task has finished,
-        // we need a way to request an unlock.  If we can't get the lock right away,
-        // this method sends an unlock task to the lock owner. This is safe for a number of reasons:
-        //   - This won't end until the calling thread has the lock, so that's a good guarantee
-        //   - If someone else takes over ownership before the caller, a new request is sent to the new owner
-        //   - The old owners ignore the unlock request if the requested surface isn't locked by them
-        //   - Deadlocks should not occur.  If a thread is waiting for a lock on a context-surface,
-        //     it will be the outer lock so the thread will have no other locks.
-        //     If a thread is waiting on an fbo-surface, it will be an inner lock. Any thread that might
-        //     want the outer context must then be getting an outer lock (so they have no locks that would
-        //     prevent this thread from getting its inner lock).
-        
-        ReentrantLock lock = surface.getLock();
-        while(!lock.tryLock()) {
-            // We do a full lock if tryLock() fails and the context manager is shutting down.
-            // This is set to true when tryLock fails, and the lifecycle is checked later.
-            boolean forceFullLockMaybe = false;
-            ContextThread owner = persistentSurfaceLocks.get(surface);
-            if (owner != null) {
-                // Send an unlock task to the thread and wait for the owner to unlock it
-                queue(new Sync<Void>(new UnlockSurfaceCallable(surface)), owner, true);
-                
-                // We don't block on the returned Future because that is linked to when the 
-                // other thread runs the unlock task. There is the chance that the surface
-                // gets unlocked within a task, so we do a timed lock.
-                // - If the timed lock fails to get it, we might queue multiple unlock tasks
-                //   to the same thread. This just means some will get ignored after the first goes through.
-
-                try {
-                    if (lock.tryLock(5, TimeUnit.MILLISECONDS)) {
-                        // Got the lock so we can break out of the loop
-                        break;
-                    } else {
-                        // Couldn't get the lock, so flag that we might need to do a regular lock
-                        forceFullLockMaybe = true;
-                    }
-                } catch(InterruptedException ie) {
-                    // We were interrupted, so we follow the same logic as if a timeout occurred
-                    forceFullLockMaybe = true;
-                }
-            }
-            
-            if (forceFullLockMaybe && lifecycleManager.isStopped()) {
-                // Looping won't work anymore so just block and things will work out
-                lock.lock();
-                break;
-            }
-        }
-        
-        // If we made it this far, we locked with tryLock() or a lock() that was forced because
-        // the context manager's lifecycle was ending, but we have the lock so we can return.
-    }
-
-    /*
-     * Utility method to queue up the task within the context of the lifecycle
-     * manager to make sure the queue action does not overlap with a status
-     * change.
-     */
-    private <T> Future<T> queue(Sync<T> task, ContextThread thread, boolean atFront) {
         // Create the Future now so that it can be easily canceled later if need be
-        Future<T> future = new FutureSync<T>(task);
+        Sync<T> sync = new Sync<T>(task);
+        Future<T> future = new FutureSync<T>(sync);
         
         lifecycleManager.getLock().lock();
         try {
-            if (!lifecycleManager.isStopped()) {
-                boolean queued = false;
-                while(!queued) {
-                    if (atFront)
-                        queued = thread.tasks.offerFirst(task);
-                    else
-                        queued = thread.tasks.offerLast(task);
-                    
-                    try {
-                        if (!queued)
-                            Thread.sleep(1);
-                    } catch(InterruptedException ie) { }
+            Status status = lifecycleManager.getStatus();
+            if (!lifecycleManager.isStopped() || (acceptOnShutdown && status == Status.STOPPING)) {
+                if (isContextThread()) {
+                    // don't queue and run the task right away
+                    sync.run();
+                } else {
+                    boolean queued = false;
+                    while(!queued) {
+                        queued = thread.tasks.offerLast(sync);
+
+                        try {
+                            if (!queued)
+                                Thread.sleep(1);
+                        } catch(InterruptedException ie) { }
+                    }
                 }
             } else {
                 // LifecycleManager is shutting down or already has been, so cancel it
     }
 
     /**
+     * Return whether or not the current Thread is a thread managed by this
+     * ContextManager and is capable of having OpenGL contexts current on it.
+     * 
+     * @return True if the calling Thread is an inner thread managed by this
+     *         ContextManager
+     */
+    public boolean isContextThread() {
+        return Thread.currentThread() == thread;
+    }
+
+    /**
      * <p>
-     * Activate the provided Surface on the current thread. The surface will be
-     * locked for the duration that it is active, or until the end of running
-     * Task. If the surface has its own OpenGLContext, that context is
-     * made current on the thread. If it does not have a context, the surface
-     * piggybacks on the last surface's context or on an internal offscreen
-     * context.
-     * </p>
+     * Activate the provided Surface on the current thread. If the surface has
+     * its own OpenGLContext, that context is made current on the thread. If it
+     * does not have a context, the surface piggybacks on the last surface's
+     * context or on an internal offscreen context.
      * <p>
-     * An activated surface that has its own context will continue to be locked
-     * after the task completes until a new surface is activated with its own
-     * context, or the {@link #lock(AbstractSurface)} method is used. Holding
-     * onto the lock across tasks allows the ContextManager to keep a context
-     * current on a single thread without needing to release it between frames.
-     * </p>
+     * An activated surface that has its own context will continue to have its
+     * context current on the thread after the task completes until a new
+     * surface is activated with its own context, or {@link
+     * forceRelease(AbstractSurface)} method is called.
      * <p>
-     * Passing in a null Surface will deactivate the currently active surface.
-     * </p>
+     * Passing in a null Surface will deactivate the currently active surface,
+     * and the layer parameter is ignored.
      * <p>
-     * This can only be called from code running on an internal thread managed
-     * by the ContextManager. The way to guarantee this is to use
-     * {@link #queue(Callable, String)}. This can be used as the ContextManager
-     * is shutting down. A destroyed surface will make the activation fail,
-     * although any previously activate surface will have already been
-     * deactivated. This assumes the surface is owned by the Framework owning
-     * this ContextManager (if this assumption is broken, undefined results will
-     * occur).
-     * </p>
+     * This can only be called from code running on the internal thread managed
+     * by the ContextManager. This assumes the surface is owned by the Framework
+     * owning this ContextManager (if this assumption is broken, undefined
+     * results will occur).
+     * <p>
+     * If <tt>surface</tt> is already destroyed, the current surface is
+     * deactivated and a null context is returned.
      * 
      * @param surface The AbstractSurface to activate
      * @param layer The layer to activate, will be passed directly to
      *            {@link AbstractSurface#onSurfaceActivate(int)}
-     * @return The OpenGLContext that is current after this surface has
-     *         been activated
+     * @return The OpenGLContext that is current after this surface has been
+     *         activated
      * @throws IllegalStateException if {@link #isContextThread()} returns false
      */
     public OpenGLContext setActiveSurface(AbstractSurface surface, int layer) {
         // Further validation is handled in the ContextThread after the lock is made
-        
         Thread current = Thread.currentThread();
-        if (current instanceof ContextThread) {
+        if (current == thread) {
             // Delegate to the thread implementation
-            return ((ContextThread) current).setActiveSurface(surface, layer);
+            return thread.setActiveSurface(surface, layer);
         } else {
             // Should never happen, these methods should be restricted to the ContextThreads
             throw new IllegalThreadStateException("setActiveSurface() cannot be called on this Thread");
         }
     }
+    
+    /**
+     * <p>
+     * Force the context thread to deactivate the given surface (if it was
+     * active), and release the surface's context if it is still current. The
+     * context may need to be released even if the surface wasn't active. If a
+     * second surface is relying on the given surface's context, that surface
+     * will be forcefully deactivated as well.
+     * <p>
+     * This can only be called on the task thread of this manager.
+     * 
+     * @param surface The surface to release
+     * @throws NullPointerException if surface is null
+     * @throws IllegalStateException if this is not the context thread
+     */
+    public void forceRelease(AbstractSurface surface) {
+        if (surface == null)
+            throw new NullPointerException("Surface cannot be null");
+        
+        Thread current = Thread.currentThread();
+        if (current == thread) {
+            // Delegate to the thread implementation
+            thread.releaseSurface(surface);
+        } else {
+            // Should never happen, these methods should be restricted to the ContextThreads
+            throw new IllegalThreadStateException("forceRelease() cannot be called on this Thread");
+        }
+    }
 
     /**
      * <p>
      * Ensure that there is a valid context current on this thread. If a surface
-     * is already active, the returned context will be the same as what was
-     * returned by {@link #setActiveSurface(AbstractSurface)}. If there is no
-     * active surface, the context will be the context of the last activated
-     * surface that had a context (assuming this thread still has the lock), or
-     * an internal context that is offscreen.
-     * </p>
+     * is already active, the returned context might be that surface's context.
+     * If there is no active surface, the context will be the context of the
+     * last activated surface that had a context (assuming this thread still has
+     * the lock), or the offscreen shared context.
      * <p>
      * Regardless, this will not return null and the returned context will
      * correctly share resources with the other contexts created by the
-     * SurfaceFactory provided to this ContextManager during construction.
-     * </p>
+     * SurfaceFactory provided to this ContextManager during initialization.
      * 
      * @return The current context
      * @throws IllegalStateException if {@link #isContextThread()} returns false
      */
     public OpenGLContext ensureContext() {
         Thread current = Thread.currentThread();
-        if (current instanceof ContextThread) {
+        if (current == thread) {
             // Delegate to the thread implementation
-            return ((ContextThread) current).ensureContext();
+            return thread.ensureContext();
         } else {
             // Should never happen, these methods should be restricted to the ContextThreads
             throw new IllegalThreadStateException("ensureContext() cannot be called on this Thread");
     /*
      * Internal thread class that manages the contexts and locks for the active
      * surface, allowing this to be used easily with a simple
-     * HardwareAccessLayer implementation. The ContextThread uses a limitless
-     * block queue to hold tasks. It can lock up to two surfaces at a time (one
-     * for being "active" and one that provides a context). When no surface can
-     * provide a context, it will lazily create a "shadow" context.
+     * HardwareAccessLayer implementation. It uses up to two surfaces at a
+     * time (one for being "active" and one that provides a context).
      */
     private class ContextThread extends Thread {
-        private OpenGLContext shadowContext; // may be null
-        private final boolean ownsShadowContext;
-
-        private AbstractSurface activeSurface;
-        private AbstractSurface contextProvider;
+        private AbstractSurface activeSurface; // active surface, might differ from contextProvider
+        private AbstractSurface contextProvider; // source of currentContext if it's not the shared context
         
-        private OpenGLContext currentContext;
+        private OpenGLContext currentContext; // non-null when a context is current
         
-        private final BlockingDeque<Sync<?>> tasks;
+        private final BlockingDeque<Sync<?>> tasks; // pending tasks
         
         public ContextThread(ThreadGroup group, String name) {
-            this(group, name, null);
-        }
-        
-        public ContextThread(ThreadGroup group, String name, OpenGLContext shadowContext) {
             super(group, name);
-            this.shadowContext = shadowContext;
-            ownsShadowContext = shadowContext == null;
             tasks = new LinkedBlockingDeque<Sync<?>>(10);
-            
             setDaemon(true);
         }
         
         public OpenGLContext ensureContext() {
             if (currentContext == null) {
-                // There is no contextProvider to piggy-back off of, so we we use the shadowContext
-                // - The shadow context is unique to this thread, so we don't need to lock
-                if (shadowContext == null)
-                    shadowContext = surfaceFactory.createShadowContext(sharedContext);
-                shadowContext.makeCurrent();
+                // There is no surface to piggy-back off of, so we we use the shared context
+                if (sharedContext == null) {
+                    // bad initialization code, a task got queued before the shared
+                    // context was created (should not happen)
+                    throw new IllegalStateException("Shared context has not been created yet");
+                }
                 
-                currentContext = shadowContext;
+                sharedContext.makeCurrent();
+                
+                currentContext = sharedContext;
                 contextProvider = null;
-            } // else shadowContext or contextProvider already give us a context
+            } // else there's a current context from somewhere, just go with it
             
             return currentContext;
         }
         public OpenGLContext setActiveSurface(AbstractSurface surface, int layer) {
             if (surface != activeSurface) {
                 // The new surface is different from the active surface, so
-                // we must deactivate and unlock the last active surface.
-                unlockActiveSurface();
+                // we must deactivate the last active surface.
+                deactivateSurface();
                 
                 // If we don't have a surface to activate, just return now 
                 // before tampering with the current context
-                if (surface == null)
+                if (surface == null || surface.isDestroyed())
                     return null;
                 
                 // Now check to see if the underlying context needs to change
                     // New surface needs its own context, so release and unlock the current context
                     releaseContext();
                     
-                    // Grab the surface's lock now as the outer context lock
-                    if (!getPersistentLock(surface))
-                        return null; // Surface is destroyed, so escape
-                    
                     // Now make its context current
                     newContext.makeCurrent();
                     currentContext = newContext;
                     ensureContext();
                 }
                 
-                // At this point we have a valid context (with an outer lock if the surface
-                // provided their own context). Now we need to get the inner lock and activate the surface
-                //  - This is a double-lock if surface.getContext() != null
-                if (!getPersistentLock(surface))
-                    return null;
-                
+                activeSurface = surface;
                 surface.onSurfaceActivate(currentContext, layer);
-                activeSurface = surface;
             } else {
-                // This is the active surface, but cycle deactivate/activate to make
-                // it notice the switch
+                // This is already the active surface, but cycle deactivate/activate
+                // to make it notice the switch
                 surface.onSurfaceDeactivate(currentContext);
                 surface.onSurfaceActivate(currentContext, layer);
             }
             return currentContext;
         }
         
-        public void unlock(AbstractSurface surface) {
-            // unlock inner lock
+        public void releaseSurface(AbstractSurface surface) {
+            // first deactivate it if it's the active surface
             if (surface == activeSurface)
-                unlockActiveSurface();
+                deactivateSurface();
             
-            // unlock outer lock
+            // then make sure we release its context
             if (surface == contextProvider)
                 releaseContext();
         }
         
-        private void unlockActiveSurface() {
+        private void deactivateSurface() {
             if (activeSurface != null) {
-                // Since activeSurface is not null, a context is current
+                // Since activeSurface is not null, a currentContext won't be null
                 activeSurface.onSurfaceDeactivate(currentContext);
+                activeSurface = null;
                 
-                if (activeSurface != contextProvider) {
-                    // The surface's lock is held once, so we need to remove it
-                    // from the persistent surface map. Must be done before we unlock
-                    persistentSurfaceLocks.remove(activeSurface);
-                }
-                
-                activeSurface.getLock().unlock();
-                activeSurface = null;
+                // we do not release the current context, even if
+                // the active surface was the context provider
             }
         }
         
         private void releaseContext() {
             if (activeSurface != null) {
                 // If we're unlocking the context, we can't have any active surface
-                unlockActiveSurface();
+                deactivateSurface();
             }
             
-            if (contextProvider != null) {
-                // The current context will be the context provider's, so release it
+            if (currentContext != null) {
                 currentContext.release();
                 currentContext = null;
-                
-                // Remove the context provider from the persistent locks map (while we still
-                // have the lock). If we're unlocking the context provider, that is the only
-                // lock the thread can have on the surface (unlike the active surface).
-                persistentSurfaceLocks.remove(contextProvider);
-                
-                contextProvider.getLock().unlock();
                 contextProvider = null;
-            } else if (currentContext != null) {
-                // The active context is from the shadow context, and it's requested that
-                // we unlock that if there was no context surface
-                currentContext.release();
-                currentContext = null;
-            }
-        }
-        
-        private boolean getPersistentLock(AbstractSurface surface) {
-            ContextManager.this.lock(surface);
-            if (surface.isDestroyed()) {
-                // The surface has been destroyed so unlock it and return false
-                surface.getLock().unlock();
-                return false;
-            } else {
-                // The surface hasn't been destroyed so record it and return true
-                persistentSurfaceLocks.put(surface, this);
-                return true;
             }
         }
         
         @Override
         public void run() {
-            while(!lifecycleManager.isStopped()) {
+            // loop until we hit WAITING_ON_CHILDREN, so that we still process 
+            // tasks while in the STOPPING stage
+            while(lifecycleManager.getStatus().compareTo(Status.WAITING_ON_CHILDREN) < 0) {
                 // Grab a single task from the queue and run it
                 // Unlocking a surface is handled by pushing a special task
                 // to the front of the queue so it skips any line from actual tasks
                 // Because all surfaces that are active must be AbstractSurfaces, this will
                 // reset all of the renderers so resources will be unlocked correctly,
                 // even in the event of an exception.
-                unlockActiveSurface();
+                deactivateSurface();
             }
             releaseContext();
             
-            if (shadowContext != null && ownsShadowContext) {
-                // This thread is the only owner of the shadow context
-                // so we don't need to lock anything before we destroy it
-                shadowContext.destroy();
-            }
+            // This thread is the owner of the shared context
+            sharedContext.destroy();
             
-            // At this point, thee task queue is no longer being modified.
+            // At this point, the task queue is no longer being modified.
             // The lifecycle manager is shutting down, so any calls to queue()
             // will have the tasks automatically canceled.
             
     }
     
     /*
-     * Simple task to force a ContextThread to fully unlock a surface.
+     * Simple task to initialize the shared context of this manager
      */
-    private class UnlockSurfaceCallable implements Callable<Void> {
-        private final AbstractSurface surface;
+    private class ConstructContextCallable implements Callable<Void> {
+        private final SurfaceFactory surfaceFactory;
         
-        public UnlockSurfaceCallable(AbstractSurface surface) {
-            this.surface = surface;
+        public ConstructContextCallable(SurfaceFactory surfaceFactory) {
+            this.surfaceFactory = surfaceFactory;
         }
         
         @Override
         public Void call() throws Exception {
-            // unlock() correctly handles when the surface isn't actually locked
-            Thread current = Thread.currentThread();
-            if (current instanceof ContextThread) {
-                // Delegate to the thread implementation
-                ((ContextThread) current).unlock(surface);
-            } else {
-                // Should never happen, these methods should be restricted to the ContextThreads
-                throw new IllegalThreadStateException("unlock() cannot be called on this thread");
-            }
+            // the context is only ever used on the thread running this
+            // task, so this assignment is thread safe
+            sharedContext = surfaceFactory.createShadowContext(null);
             return null;
         }
     }

File ferox-renderer/ferox-renderer-impl/src/main/java/com/ferox/renderer/impl/LifeCycleManager.java

 import com.ferox.renderer.Framework;
 
 /**
+ * <p>
  * The LifeCycleManager is a utility to provide a thread-safe mechanism that
  * imposes a single life cycle on a number of related components. It is assumed
  * that there is an owning object (such as the {@link Framework} implementation)
  * that controls the creation of the LifeCycleManager and exposes a public
- * interface to begin the life cycle.</p>
+ * interface to begin the life cycle.
+ * </p>
  * <p>
  * The LifeCycleManager provides two methods to change its status. The method
  * {@link #start(Runnable)} is used to start the manager and
  * allow custom code to be run during these state transitions. The owner of the
  * LifeCycleManager would use these to initialize all components that it
  * depended on.
- * </p>
  * 
  * @author Michael Ludwig
  */
-//FIXME: we could use the LifeCycleManager to do automatic clean-up
-// of the owning Framework if we set things up correctly
 public class LifeCycleManager {
     /**
      * A LifeCycleManager has a monotonically increasing status. The status will
      * states.
      */
     public static enum Status {
-        WAITING_INIT, STARTING, ACTIVE, STOPPING, STOPPED
+        WAITING_INIT, STARTING, ACTIVE, STOPPING, WAITING_ON_CHILDREN, STOPPED
     }
     
     private final ReentrantReadWriteLock lock;
      */
     public boolean isStopped() {
         Status status = this.status;
-        return status == Status.STOPPING || status == Status.STOPPED;
+        return status == Status.STOPPING || status == Status.STOPPED || status == Status.WAITING_ON_CHILDREN;
     }
     
     /**
      * status from WAITING_INIT to STARTING to ACTIVE. This can only be called
      * once. The first time this is invoked, true is returned. All other
      * invocations return false and do not change the status of the manager.
-     * </p>
      * <p>
      * <tt>onInit</tt> is invoked only if true will be returned (i.e. the first
      * time this is called), and should contain framework level code to be
      * threads or to initialize subcomponents that exist within this managed
      * lifecycle. While the provided Runnable is running, the manager has a
      * status of STARTING. When the Runnable completes, this changes to ACTIVE.
-     * </p>
      * <p>
      * The provided Runnable must be "trusted" code and should not throw
-     * exceptions or the manager will be trapped in STARTING.
-     * </p>
+     * exceptions or the manager will be trapped in STARTING. The runnable must
+     * be thread safe so that it can safely initialize the system from whatever
+     * thread invoked start().
      * 
      * @param onInit A Runnable to run initialization code within an exclusive
      *            lock on the lifecycle
      * @return True if the manager was successfully started
-     * @throws NullPointerException if onInit is null
      */
     public boolean start(Runnable onInit) {
-        if (onInit == null)
-            throw new NullPointerException("onInit cannot be null");
-        
         lock.writeLock().lock();
         try {
             if (status != Status.WAITING_INIT)
                 return false;
             status = Status.STARTING;
-            onInit.run();
+            if (onInit != null)
+                onInit.run();
             status = Status.ACTIVE;
             return true;
         } finally {
      * Stop or destroy this LifeCycleManager. The status transitions depends on
      * the current state of the manager. Like {@link #start(Runnable)}, this can
      * only be invoked once and all future calls do nothing except return false.
-     * </p>
      * <p>
      * If the manager is WAITING_INIT, its status changes directly to STOPPED
-     * and does not run the Runnable, <tt>onDestroy</tt>. If the manager is
-     * ACTIVE, its status changes to STOPPING and it starts a new thread that
-     * will eventually run the code in <tt>onDestroy</tt>. The new thread will
-     * first block until all managed threads have terminated. After the threads
-     * have finished, <tt>onDestroy</tt> is run and the status is changed to
-     * STOPPED.
-     * </p>
+     * and does not run either Runnable. If the status is ACTIVE, it first runs
+     * <tt>preDestroy</tt>, then changes status changes to STOPPING. The manager
+     * then starts a new thread that will eventually run the code in
+     * <tt>onDestroy</tt>. The new thread will first block until all managed
+     * threads have terminated. After the threads have finished,
+     * <tt>onDestroy</tt> is run and the status is changed to STOPPED.
      * <p>
      * A value of true is returned the first time this is invoked. A value of
      * false is returned if the manager is stopping, has stopped or is starting.
      * Calls to this method while the status is STARTING return false and do
      * nothing.
-     * </p>
      * <p>
      * The provided Runnable must be "trusted" code and should not throw
-     * exceptions or the manager will be trapped in STARTING.
-     * </p>
+     * exceptions or the manager's state will be undefined. <tt>preDestroy</tt>
+     * must be thread safe so that it can be safely called from whatever thread
+     * invoked stop(). <tt>postDestroy</tt> must be safe to call from the
+     * shutdown thread that is started by this manager.
      * 
-     * @param onDestroy A Runnable to perform the destruction code at the end of
-     *            the lifecycle
+     * @param stopping A Runnable executed after the exclusive lock is held,
+     *            but before the state transitions to STOPPING
      * @return True if the manager transitions to STOPPING or STOPPED and false
      *         otherwise
-     * @throws NullPointerException if onDestroy is null
+     * @throws NullPointerException if either runnable is null
      */
-    public boolean stop(Runnable onDestroy) {
+    public boolean stop(Runnable preDestroy) {
         lock.writeLock().lock();
         try {
             // Cannot destroy if actively being started, destroyed or has already been destroyed
-            if (status == Status.STOPPED || status == Status.STOPPING || status == Status.STARTING)
+            if (status != Status.WAITING_INIT && status != Status.ACTIVE)
                 return false;
             
             if (status == Status.WAITING_INIT) {
-                // never initialized, so just stop w/o running code
+                // never initialized
                 status = Status.STOPPED;
                 return true;
             } else {
                 // status must be ACTIVE, so start a shutdown thread
                 status = Status.STOPPING;
-
+                
+                // invoke this task while STOPPING, but before we block on children
+                if (preDestroy != null)
+                    preDestroy.run();
+                status = Status.WAITING_ON_CHILDREN;
+                
                 ThreadGroup shutdownOwner = Thread.currentThread().getThreadGroup();
                 while(managedThreadGroup.parentOf(shutdownOwner)) {
                     // The shutdown thread joins on threads within the managedThreads group,
                     shutdownOwner = shutdownOwner.getParent();
                 }