diff options
author | Sven Gothel <[email protected]> | 2013-01-11 08:13:24 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2013-01-11 08:13:24 +0100 |
commit | 224fab1b2c71464826594740022fdcbe278867dc (patch) | |
tree | fb4292f8cd8b8fa2664c04005ba2ddde630d159c /src/jogl | |
parent | 6fd9c3d84e1758ae27cd10a89237a558460ca1fb (diff) |
GLAutoDrawable/AnimatorBase: Add ExclusiveContextThread (ECT) feature; AnimatorBase: Add setModeBits/MODE_EXPECT_AWT_RENDERING_THREAD; FPSAnimator: Make transactions deterministic.
ExclusiveContextThread (ECT) allows user to dedicate a GLContext to a given thread.
Only the ECT will be allowed to claim the GLContext, hence releasing must be done on the ECT itself.
The core feature is accessible via GLAutoDrawable, while it can be conveniently enabled and disabled
via an AnimatorBase implementation. The latter ensures it's being released on the ECT and waits for the result.
Note that ECT cannot be guaranteed to work correctly w/ native (heavyweight) AWT components
due to resource locking and AWT-EDT access. This is disabled in all new tests per default and
noted on the API doc.
Note: 'Animator transaction' == start(), stop(), pause(), resume().
- Add ExclusiveContextThread (ECT) feature
- GLAutoDrawable NEW:
- Thread setExclusiveContextThread(Thread t)
- Thread getExclusiveContextThread()
- AnimatorBase NEW:
- Thread setExclusiveContext(Thread t)
- boolean setExclusiveContext(boolean enable)
- boolean isExclusiveContextEnabled()
- Thread getExclusiveContextThread()
- AnimatorBase: Add setModeBits/MODE_EXPECT_AWT_RENDERING_THREAD
Allows user to pre-determine whether AWT rendering is expected before starting the animator.
If AWT is excluded, a more simple and transaction correct impl. will be used.
- FPSAnimator: Make transactions deterministic.
FPSAnimator previously did not ensure whether a transaction was completed.
A deterministic transaction is required to utilize ECT.
FPSAnimator now uses same mechanism like Animator to ensure completeness,
i.e. Condition and 'finishLifecycleAction(..)'. Both are moved to AnimatorBase.
Tested manually on Linux/NV, Linux/AMD, Windows/NV and OSX/NV.
- All new tests validated correctness.
- All new tests shows an performance increase of ~3x w/ single GLWindow, where multiple GLWindows don't show a perf. increase.
Diffstat (limited to 'src/jogl')
11 files changed, 1093 insertions, 414 deletions
diff --git a/src/jogl/classes/com/jogamp/opengl/swt/GLCanvas.java b/src/jogl/classes/com/jogamp/opengl/swt/GLCanvas.java index 4ba4def9a..80e1aa80d 100644 --- a/src/jogl/classes/com/jogamp/opengl/swt/GLCanvas.java +++ b/src/jogl/classes/com/jogamp/opengl/swt/GLCanvas.java @@ -649,6 +649,16 @@ public class GLCanvas extends Canvas implements GLAutoDrawable { } @Override + public final Thread setExclusiveContextThread(Thread t) throws GLException { + return helper.setExclusiveContextThread(t, context); + } + + @Override + public final Thread getExclusiveContextThread() { + return helper.getExclusiveContextThread(); + } + + @Override public boolean getAutoSwapBufferMode() { return helper.getAutoSwapBufferMode(); } diff --git a/src/jogl/classes/com/jogamp/opengl/util/Animator.java b/src/jogl/classes/com/jogamp/opengl/util/Animator.java index ddbc66fa3..10f43a0c1 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/Animator.java +++ b/src/jogl/classes/com/jogamp/opengl/util/Animator.java @@ -41,7 +41,7 @@ package com.jogamp.opengl.util; import javax.media.opengl.GLAutoDrawable; - +import javax.media.opengl.GLException; /** <P> An Animator can be attached to one or more {@link GLAutoDrawable}s to drive their display() methods in a loop. </P> @@ -57,10 +57,7 @@ import javax.media.opengl.GLAutoDrawable; * Call {@link #stop() } to terminate the animation and it's execution thread. * </p> */ -public class Animator extends AnimatorBase { - /** timeout in milliseconds, 15 frames @ 60Hz = 240ms, limiting {@link #finishLifecycleAction(Condition)} */ - private static final long TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION = 15*16; - +public class Animator extends AnimatorBase { protected ThreadGroup threadGroup; private Runnable runnable; private boolean runAsFastAsPossible; @@ -68,6 +65,9 @@ public class Animator extends AnimatorBase { protected boolean pauseIssued; protected volatile boolean stopIssued; + /** + * Creates a new, empty Animator. + */ public Animator() { super(); if(DEBUG) { @@ -75,25 +75,38 @@ public class Animator extends AnimatorBase { } } + /** + * Creates a new Animator w/ an associated ThreadGroup. + */ public Animator(ThreadGroup tg) { super(); - threadGroup = tg; - + setThreadGroup(tg); if(DEBUG) { System.err.println("Animator created, ThreadGroup: "+threadGroup); } } - /** Creates a new Animator for a particular drawable. */ + /** + * Creates a new Animator for a particular drawable. + */ public Animator(GLAutoDrawable drawable) { super(); add(drawable); + if(DEBUG) { + System.err.println("Animator created, w/ "+drawable); + } } - /** Creates a new Animator for a particular drawable. */ + /** + * Creates a new Animator w/ an associated ThreadGroup for a particular drawable. + */ public Animator(ThreadGroup tg, GLAutoDrawable drawable) { - this(tg); + super(); + setThreadGroup(tg); add(drawable); + if(DEBUG) { + System.err.println("Animator created, ThreadGroup: "+threadGroup+" and "+drawable); + } } protected String getBaseName(String prefix) { @@ -114,7 +127,7 @@ public class Animator extends AnimatorBase { stateSync.unlock(); } } - + private final void setIsAnimatingSynced(boolean v) { stateSync.lock(); try { @@ -126,36 +139,39 @@ public class Animator extends AnimatorBase { class MainLoop implements Runnable { public String toString() { - return "[started "+isStartedImpl()+", animating "+isAnimatingImpl()+", paused "+isPausedImpl()+", drawable "+drawables.size()+"]"; + return "[started "+isStartedImpl()+", animating "+isAnimatingImpl()+", paused "+isPausedImpl()+", drawable "+drawables.size()+", drawablesEmpty "+drawablesEmpty+"]"; } public void run() { try { - synchronized (Animator.this) { - if(DEBUG) { - System.err.println("Animator start:" + Thread.currentThread() + ": " + toString()); - } - fpsCounter.resetFPSCounter(); - animThread = Thread.currentThread(); - setIsAnimatingSynced(false); // barrier - Animator.this.notifyAll(); + if(DEBUG) { + System.err.println("Animator start:" + Thread.currentThread() + ": " + toString()); } + fpsCounter.resetFPSCounter(); + animThread = Thread.currentThread(); + setIsAnimatingSynced(false); // barrier + // 'waitForStartedCondition' wake-up is handled below! while (!stopIssued) { synchronized (Animator.this) { - // Don't consume CPU unless there is work to be done and not paused + // Pause; Also don't consume CPU unless there is work to be done and not paused + boolean ectCleared = false; while (!stopIssued && (pauseIssued || drawablesEmpty)) { boolean wasPaused = pauseIssued; if (DEBUG) { System.err.println("Animator pause:" + Thread.currentThread() + ": " + toString()); } + if ( exclusiveContext && !drawablesEmpty && !ectCleared ) { + ectCleared = true; + setDrawablesExclCtxState(false); + display(); // propagate exclusive change! + } setIsAnimatingSynced(false); // barrier Animator.this.notifyAll(); try { Animator.this.wait(); } catch (InterruptedException e) { } - if (wasPaused) { // resume from pause -> reset counter fpsCounter.resetFPSCounter(); @@ -165,9 +181,12 @@ public class Animator extends AnimatorBase { } } if (!stopIssued && !isAnimating) { - // resume from pause or drawablesEmpty, + // Wakes up 'waitForStartedCondition' sync + // - and - + // Resume from pause or drawablesEmpty, // implies !pauseIssued and !drawablesEmpty - setIsAnimatingSynced(true); + setIsAnimatingSynced(true); // barrier + setDrawablesExclCtxState(exclusiveContext); Animator.this.notifyAll(); } } // sync Animator.this @@ -180,6 +199,10 @@ public class Animator extends AnimatorBase { } } } finally { + if( exclusiveContext && !drawablesEmpty ) { + setDrawablesExclCtxState(false); + display(); // propagate exclusive change! + } synchronized (Animator.this) { if(DEBUG) { System.err.println("Animator stop " + Thread.currentThread() + ": " + toString()); @@ -194,18 +217,6 @@ public class Animator extends AnimatorBase { } } - private final boolean isStartedImpl() { - return animThread != null ; - } - public final boolean isStarted() { - stateSync.lock(); - try { - return animThread != null ; - } finally { - stateSync.unlock(); - } - } - private final boolean isAnimatingImpl() { return animThread != null && isAnimating ; } @@ -230,39 +241,19 @@ public class Animator extends AnimatorBase { } } - interface Condition { - /** - * @return true if branching (cont waiting, action), otherwise false - */ - boolean result(); - } - - private synchronized void finishLifecycleAction(Condition condition) { - // It's hard to tell whether the thread which changes the lifecycle has - // dependencies on the Animator's internal thread. Currently we - // use a couple of heuristics to determine whether we should do - // the blocking wait(). - final boolean blocking = impl.blockUntilDone(animThread); - long remaining = blocking ? TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION : 0; - while (remaining>0 && condition.result()) { - long td = System.currentTimeMillis(); - try { - wait(remaining); - } catch (InterruptedException ie) { } - remaining -= (System.currentTimeMillis() - td) ; - } - if(DEBUG) { - if(remaining<0) { - System.err.println("finishLifecycleAction(" + condition.getClass().getName() + "): ++++++ timeout reached ++++++ " + Thread.currentThread().getName()); - } - System.err.println("finishLifecycleAction(" + condition.getClass().getName() + "): finished "+ - "- blocking "+blocking+ - ", waited " + (blocking ? ( TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION - remaining ) : 0 ) + "/" + TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION + - ", started: " + isStartedImpl() +", animating: " + isAnimatingImpl() + - ", paused: " + isPausedImpl() + ", drawables " + drawables.size() + " - " + Thread.currentThread().getName()); + /** + * Set a {@link ThreadGroup} for the {@link #getThread() animation thread}. + * + * @param tg the {@link ThreadGroup} + * @throws GLException if the animator has already been started + */ + public synchronized void setThreadGroup(ThreadGroup tg) throws GLException { + if ( isStartedImpl() ) { + throw new GLException("Animator already started."); } + threadGroup = tg; } - + public synchronized boolean start() { if ( isStartedImpl() ) { return false; @@ -284,74 +275,48 @@ public class Animator extends AnimatorBase { System.err.println("Animator "+ct.getName()+"[daemon "+ct.isDaemon()+"]: starting "+thread.getName()+"[daemon "+thread.isDaemon()+"]"); } thread.start(); - finishLifecycleAction(waitForStartedCondition); - return true; + return finishLifecycleAction(waitForStartedCondition, 0); } - - private class WaitForStartedCondition implements Condition { - public boolean result() { + private final Condition waitForStartedCondition = new Condition() { + public boolean eval() { return !isStartedImpl() || (!drawablesEmpty && !isAnimating) ; - } - } - Condition waitForStartedCondition = new WaitForStartedCondition(); + } }; public synchronized boolean stop() { if ( !isStartedImpl() ) { return false; } stopIssued = true; - notifyAll(); - finishLifecycleAction(waitForStoppedCondition); - return true; + return finishLifecycleAction(waitForStoppedCondition, 0); } - private class WaitForStoppedCondition implements Condition { - public boolean result() { + private final Condition waitForStoppedCondition = new Condition() { + public boolean eval() { return isStartedImpl(); - } - } - Condition waitForStoppedCondition = new WaitForStoppedCondition(); + } }; public synchronized boolean pause() { if ( !isStartedImpl() || pauseIssued ) { return false; } - stateSync.lock(); - try { - pauseIssued = true; - } finally { - stateSync.unlock(); - } - notifyAll(); - finishLifecycleAction(waitForPausedCondition); - return true; + pauseIssued = true; + return finishLifecycleAction(waitForPausedCondition, 0); } - private class WaitForPausedCondition implements Condition { - public boolean result() { + private final Condition waitForPausedCondition = new Condition() { + public boolean eval() { // end waiting if stopped as well return isAnimating && isStartedImpl(); - } - } - Condition waitForPausedCondition = new WaitForPausedCondition(); + } }; public synchronized boolean resume() { if ( !isStartedImpl() || !pauseIssued ) { return false; } - stateSync.lock(); - try { - pauseIssued = false; - } finally { - stateSync.unlock(); - } - notifyAll(); - finishLifecycleAction(waitForResumeCondition); - return true; + pauseIssued = false; + return finishLifecycleAction(waitForResumeCondition, 0); } - private class WaitForResumeCondition implements Condition { - public boolean result() { + private final Condition waitForResumeCondition = new Condition() { + public boolean eval() { // end waiting if stopped as well return !drawablesEmpty && !isAnimating && isStartedImpl(); - } - } - Condition waitForResumeCondition = new WaitForResumeCondition(); + } }; } diff --git a/src/jogl/classes/com/jogamp/opengl/util/AnimatorBase.java b/src/jogl/classes/com/jogamp/opengl/util/AnimatorBase.java index 46fc1d991..bda716e97 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/AnimatorBase.java +++ b/src/jogl/classes/com/jogamp/opengl/util/AnimatorBase.java @@ -38,6 +38,7 @@ import java.util.ArrayList; import javax.media.opengl.GLAnimatorControl; import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLException; import javax.media.opengl.GLProfile; /** @@ -51,69 +52,174 @@ import javax.media.opengl.GLProfile; */ public abstract class AnimatorBase implements GLAnimatorControl { protected static final boolean DEBUG = Debug.debug("Animator"); - - private static int animatorCount = 0; - + + /** A 1s timeout while waiting for a native action response, limiting {@link #finishLifecycleAction(Condition, long)} */ + protected static final long TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION = 1000; + + protected static final long POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION = 32; // 2 frames @ 60Hz + + /** + * If present in <code>modeBits</code> field and + * {@link GLProfile#isAWTAvailable() AWT is available}, + * implementation is aware of the AWT EDT, otherwise not. + * <p> + * This is the <i>default</i>. + * </p> + * @see #setModeBits(boolean, int) + */ + public static final int MODE_EXPECT_AWT_RENDERING_THREAD = 1 << 0; + public interface AnimatorImpl { void display(ArrayList<GLAutoDrawable> drawables, boolean ignoreExceptions, boolean printExceptions); boolean blockUntilDone(Thread thread); } - protected ArrayList<GLAutoDrawable> drawables = new ArrayList<GLAutoDrawable>(); - protected boolean drawablesEmpty; + protected int modeBits; protected AnimatorImpl impl; protected String baseName; + + protected ArrayList<GLAutoDrawable> drawables = new ArrayList<GLAutoDrawable>(); + protected boolean drawablesEmpty; protected Thread animThread; protected boolean ignoreExceptions; protected boolean printExceptions; + protected boolean exclusiveContext; + protected Thread userExclusiveContextThread; protected FPSCounterImpl fpsCounter = new FPSCounterImpl(); protected RecursiveLock stateSync = LockFactory.createRecursiveLock(); - - /** Creates a new, empty Animator. */ - public AnimatorBase() { - if(GLProfile.isAWTAvailable()) { + + private final static Class<?> awtAnimatorImplClazz; + static { + GLProfile.initSingleton(); + if( GLProfile.isAWTAvailable() ) { + Class<?> clazz; try { - impl = (AnimatorImpl) Class.forName("com.jogamp.opengl.util.AWTAnimatorImpl").newInstance(); - baseName = "AWTAnimator"; - } catch (Exception e) { e.printStackTrace(); } - } - if(null==impl) { - impl = new DefaultAnimatorImpl(); - baseName = "Animator"; - } - synchronized (Animator.class) { - animatorCount++; - baseName = baseName.concat("-"+animatorCount); - drawablesEmpty = true; + clazz = Class.forName("com.jogamp.opengl.util.AWTAnimatorImpl"); + } catch (Exception e) { + clazz = null; + } + awtAnimatorImplClazz = clazz; + } else { + awtAnimatorImplClazz = null; } } + /** + * Creates a new, empty Animator instance + * while expecting an AWT rendering thread if AWT is available. + * + * @see GLProfile#isAWTAvailable() + */ + public AnimatorBase() { + modeBits = MODE_EXPECT_AWT_RENDERING_THREAD; // default! + drawablesEmpty = true; + } + + /** + * Initializes implementation details post setup, + * invoked at {@link #add(GLAutoDrawable)}, {@link #start()}, .. + * <p> + * Operation is a NOP if <code>force</code> is <code>false</code> + * and this instance is already initialized. + * </p> + * + * @throws GLException if Animator is {@link #isStarted()} + */ + protected void initImpl(boolean force) { + if( force || null == impl ) { + if( 0 != ( MODE_EXPECT_AWT_RENDERING_THREAD & modeBits ) && null != awtAnimatorImplClazz ) { + try { + impl = (AnimatorImpl) awtAnimatorImplClazz.newInstance(); + baseName = getBaseName("AWT"); + } catch (Exception e) { e.printStackTrace(); } + } + if( null == impl ) { + impl = new DefaultAnimatorImpl(); + baseName = getBaseName(""); + } + if(DEBUG) { + System.err.println("Animator.initImpl: baseName "+baseName+", implClazz "+impl.getClass().getName()+" - "+toString()+" - "+Thread.currentThread().getName()); + } + } + } protected abstract String getBaseName(String prefix); - public synchronized void add(GLAutoDrawable drawable) { + /** + * Enables or disables the given <code>bitValues</code> + * in this Animators <code>modeBits</code>. + * @param enable + * @param bitValues + * + * @throws GLException if Animator is {@link #isStarted()} + * @see AnimatorBase#MODE_EXPECT_AWT_RENDERING_THREAD + */ + public synchronized void setModeBits(boolean enable, int bitValues) throws GLException { + if( isStarted() ) { + throw new GLException("Animator already started"); + } + final int _oldModeBits = modeBits; + if(enable) { + modeBits |= bitValues; + } else { + modeBits &= ~bitValues; + } + if( _oldModeBits != modeBits ) { + initImpl(true); + } + } + public synchronized int getModeBits() { return modeBits; } + + + @Override + public synchronized void add(final GLAutoDrawable drawable) { if(DEBUG) { - System.err.println("Animator add: "+drawable.hashCode()+" - "+Thread.currentThread().getName()); + System.err.println("Animator add: 0x"+Integer.toHexString(drawable.hashCode())+" - "+toString()+" - "+Thread.currentThread().getName()); } + if( drawables.contains(drawable) ) { + throw new IllegalArgumentException("Drawable already added to animator: "+this+", "+drawable); + } + initImpl(false); boolean paused = pause(); + if( isStarted() ) { + drawable.setExclusiveContextThread( exclusiveContext ? getExclusiveContextThread() : null ); // if already running .. + } drawables.add(drawable); drawablesEmpty = drawables.size() == 0; drawable.setAnimator(this); if(paused) { resume(); } - if(impl.blockUntilDone(animThread)) { - while(isStarted() && !isPaused() && !isAnimating()) { - try { - wait(); - } catch (InterruptedException ie) { } - } + final Condition waitForAnimatingAndECTCondition = new Condition() { + public boolean eval() { + final Thread dect = drawable.getExclusiveContextThread(); + return isStarted() && !isPaused() && !isAnimating() && ( exclusiveContext && null == dect || !exclusiveContext && null != dect ); + } }; + final boolean res = finishLifecycleAction(waitForAnimatingAndECTCondition, 0); + if(DEBUG) { + System.err.println("Animator add: Wait for Animating/ECT OK: "+res+", "+toString()+", dect "+drawable.getExclusiveContextThread()); } notifyAll(); } - public synchronized void remove(GLAutoDrawable drawable) { + @Override + public synchronized void remove(final GLAutoDrawable drawable) { if(DEBUG) { - System.err.println("Animator remove: "+drawable.hashCode()+" - "+Thread.currentThread().getName() + ": "+toString()); + System.err.println("Animator remove: 0x"+Integer.toHexString(drawable.hashCode())+" - "+toString()+" - "+Thread.currentThread().getName()); + } + if( !drawables.contains(drawable) ) { + throw new IllegalArgumentException("Drawable not added to animator: "+this+", "+drawable); + } + + if( exclusiveContext && isAnimating() ) { + drawable.setExclusiveContextThread( null ); + final Condition waitForNullECTCondition = new Condition() { + public boolean eval() { + return null != drawable.getExclusiveContextThread(); + } }; + final boolean res = finishLifecycleAction(waitForNullECTCondition, POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION); + if(DEBUG) { + System.err.println("Animator remove: Wait for Null-ECT OK: "+res+", "+toString()+", dect "+drawable.getExclusiveContextThread()); + } } boolean paused = pause(); @@ -123,14 +229,202 @@ public abstract class AnimatorBase implements GLAnimatorControl { if(paused) { resume(); } - if(impl.blockUntilDone(animThread)) { - while(isStarted() && drawablesEmpty && isAnimating()) { + final boolean res = finishLifecycleAction(waitForNotAnimatingIfEmptyCondition, 0); + if(DEBUG) { + System.err.println("Animator remove: Wait for !Animating-if-empty OK: "+res+", "+toString()); + } + notifyAll(); + } + private final Condition waitForNotAnimatingIfEmptyCondition = new Condition() { + public boolean eval() { + return isStarted() && drawablesEmpty && isAnimating(); + } }; + + + /** + * Dedicate all {@link GLAutoDrawable}'s context to the given exclusive context thread. + * <p> + * The given thread will be exclusive to all {@link GLAutoDrawable}'s context while {@link #isAnimating()}. + * </p> + * <p> + * If already started and disabling, method waits + * until change is propagated to all {@link GLAutoDrawable} if not + * called from the animator thread or {@link #getExclusiveContextThread() exclusive context thread}. + * </p> + * <p> + * Note: Utilizing this feature w/ AWT could lead to an AWT-EDT deadlock, depending on the AWT implementation. + * Hence it is advised not to use it with native AWT GLAutoDrawable like GLCanvas. + * </p> + * + * @param enable + * @return previous value + * @see #setExclusiveContext(boolean) + * @see #getExclusiveContextThread() + * @see #isExclusiveContextEnabled() + */ + // @Override + public final Thread setExclusiveContext(Thread t) { + final Thread old; + final boolean enable = null != t; + stateSync.lock(); + try { + old = userExclusiveContextThread; + if( enable && t != animThread ) { // disable: will be cleared at end after propagation && filter out own animThread usae + userExclusiveContextThread=t; + } + } finally { + stateSync.unlock(); + } + setExclusiveContext(enable); + return old; + } + + /** + * Dedicate all {@link GLAutoDrawable}'s context to this animator thread. + * <p> + * The given thread will be exclusive to all {@link GLAutoDrawable}'s context while {@link #isAnimating()}. + * </p> + * <p> + * If already started and disabling, method waits + * until change is propagated to all {@link GLAutoDrawable} if not + * called from the animator thread or {@link #getExclusiveContextThread() exclusive context thread}. + * </p> + * <p> + * Note: Utilizing this feature w/ AWT could lead to an AWT-EDT deadlock, depending on the AWT implementation. + * Hence it is advised not to use it with native AWT GLAutoDrawable like GLCanvas. + * </p> + * + * @param enable + * @return previous value + * @see #setExclusiveContext(Thread) + * @see #getExclusiveContextThread() + * @see #isExclusiveContextEnabled() + */ + // @Override + public final boolean setExclusiveContext(boolean enable) { + final boolean propagateState; + final boolean oldExclusiveContext; + final Thread _exclusiveContextThread; + synchronized (AnimatorBase.this) { + propagateState = isStarted() && !drawablesEmpty; + _exclusiveContextThread = userExclusiveContextThread; + oldExclusiveContext = exclusiveContext; + exclusiveContext = enable; + if(DEBUG) { + System.err.println("AnimatorBase.setExclusiveContextThread: "+oldExclusiveContext+" -> "+exclusiveContext+", propagateState "+propagateState+", "+this); + } + } + final Thread dECT = enable ? ( null != userExclusiveContextThread ? userExclusiveContextThread : animThread ) : null ; + if( propagateState ) { + setDrawablesExclCtxState(enable); + if( !enable ) { + if( Thread.currentThread() == getThread() || Thread.currentThread() == _exclusiveContextThread ) { + display(); + } else { + final boolean resumed = isAnimating() ? false : resume(); + int counter = 10; + while( 0<counter && isAnimating() && !validateDrawablesExclCtxState(dECT) ) { + try { + Thread.sleep(20); + } catch (InterruptedException e) { } + counter--; + } + if(resumed) { + pause(); + } + } + stateSync.lock(); try { - wait(); - } catch (InterruptedException ie) { } + userExclusiveContextThread=null; + } finally { + stateSync.unlock(); + } } } - notifyAll(); + if(DEBUG) { + System.err.println("AnimatorBase.setExclusiveContextThread: all-GLAD Ok: "+validateDrawablesExclCtxState(dECT)+", "+this); + } + return oldExclusiveContext; + } + + /** + * Returns <code>true</code>, if the exclusive context thread is enabled, otherwise <code>false</code>. + * + * @see #setExclusiveContext(boolean) + * @see #setExclusiveContext(Thread) + */ + // @Override + public final boolean isExclusiveContextEnabled() { + stateSync.lock(); + try { + return exclusiveContext; + } finally { + stateSync.unlock(); + } + } + + /** + * Returns the exclusive context thread if {@link #isExclusiveContextEnabled()} and {@link #isStarted()}, otherwise <code>null</code>. + * <p> + * If exclusive context is enabled via {@link #setExclusiveContext(boolean)} + * the {@link #getThread() animator thread} is returned if above conditions are met. + * </p> + * <p> + * If exclusive context is enabled via {@link #setExclusiveContext(Thread)} + * the user passed thread is returned if above conditions are met. + * </p> + * @see #setExclusiveContext(boolean) + * @see #setExclusiveContext(Thread) + */ + // @Override + public final Thread getExclusiveContextThread() { + stateSync.lock(); + try { + return ( isStartedImpl() && exclusiveContext ) ? ( null != userExclusiveContextThread ? userExclusiveContextThread : animThread ) : null ; + } finally { + stateSync.unlock(); + } + } + + /** + * Should be called at {@link #start()} and {@link #stop()} + * from within the animator thread. + * <p> + * At {@link #stop()} an additional {@link #display()} call shall be issued + * to allow propagation of releasing the exclusive thread. + * </p> + */ + protected final synchronized void setDrawablesExclCtxState(boolean enable) { + if(DEBUG) { + System.err.println("AnimatorBase.setExclusiveContextImpl exlusive "+exclusiveContext+": Enable "+enable+" for "+this+" - "+Thread.currentThread()); + // Thread.dumpStack(); + } + final Thread ect = getExclusiveContextThread(); + for (int i=0; i<drawables.size(); i++) { + try { + drawables.get(i).setExclusiveContextThread( enable ? ect : null ); + } catch (RuntimeException e) { + e.printStackTrace(); + } + } + } + protected final boolean validateDrawablesExclCtxState(Thread expected) { + for (int i=0; i<drawables.size(); i++) { + if( expected != drawables.get(i).getExclusiveContextThread() ) { + return false; + } + } + return true; + } + + @Override + public final Thread getThread() { + stateSync.lock(); + try { + return animThread; + } finally { + stateSync.unlock(); + } } /** Called every frame to cause redrawing of all of the @@ -143,55 +437,56 @@ public abstract class AnimatorBase implements GLAnimatorControl { fpsCounter.tickFPS(); } + @Override public final void setUpdateFPSFrames(int frames, PrintStream out) { fpsCounter.setUpdateFPSFrames(frames, out); } + @Override public final void resetFPSCounter() { fpsCounter.resetFPSCounter(); } + @Override public final int getUpdateFPSFrames() { return fpsCounter.getUpdateFPSFrames(); } + @Override public final long getFPSStartTime() { return fpsCounter.getFPSStartTime(); } + @Override public final long getLastFPSUpdateTime() { return fpsCounter.getLastFPSUpdateTime(); } + @Override public final long getLastFPSPeriod() { return fpsCounter.getLastFPSPeriod(); } + @Override public final float getLastFPS() { return fpsCounter.getLastFPS(); } + @Override public final int getTotalFPSFrames() { return fpsCounter.getTotalFPSFrames(); } + @Override public final long getTotalFPSDuration() { return fpsCounter.getTotalFPSDuration(); } + @Override public final float getTotalFPS() { return fpsCounter.getTotalFPS(); } - public final Thread getThread() { - stateSync.lock(); - try { - return animThread; - } finally { - stateSync.unlock(); - } - } - /** Sets a flag causing this Animator to ignore exceptions produced while redrawing the drawables. By default this flag is set to false, causing any exception thrown to halt the Animator. */ @@ -207,7 +502,78 @@ public abstract class AnimatorBase implements GLAnimatorControl { this.printExceptions = printExceptions; } + protected interface Condition { + /** + * @return true if branching (continue waiting, action), otherwise false + */ + boolean eval(); + } + + /** + * @param waitCondition method will wait until TO is reached or {@link Condition#eval() waitCondition.eval()} returns <code>false</code>. + * @param pollPeriod if <code>0</code>, method will wait until TO is reached or being notified. + * if > <code>0</code>, method will wait for the given <code>pollPeriod</code> in milliseconds. + * @return <code>true</code> if {@link Condition#eval() waitCondition.eval()} returned <code>false</code>, otherwise <code>false</code>. + */ + protected synchronized boolean finishLifecycleAction(Condition waitCondition, long pollPeriod) { + // It's hard to tell whether the thread which changes the lifecycle has + // dependencies on the Animator's internal thread. Currently we + // use a couple of heuristics to determine whether we should do + // the blocking wait(). + initImpl(false); + final boolean blocking = impl.blockUntilDone(animThread); + long remaining = blocking ? TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION : 0; + if( 0 >= pollPeriod ) { + pollPeriod = remaining; + } + boolean nok = waitCondition.eval(); + while ( nok && remaining>0 ) { + final long t1 = System.currentTimeMillis(); + if( pollPeriod > remaining ) { pollPeriod = remaining; } + notifyAll(); + try { + wait(pollPeriod); + } catch (InterruptedException ie) { } + remaining -= System.currentTimeMillis() - t1 ; + nok = waitCondition.eval(); + } + if(DEBUG || nok) { + if( remaining<=0 && nok ) { + System.err.println("finishLifecycleAction(" + waitCondition.getClass().getName() + "): ++++++ timeout reached ++++++ " + Thread.currentThread().getName()); + } + stateSync.lock(); // avoid too many lock/unlock ops + try { + System.err.println("finishLifecycleAction(" + waitCondition.getClass().getName() + "): OK "+(!nok)+ + "- pollPeriod "+pollPeriod+", blocking "+blocking+ + ", waited " + (blocking ? ( TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION - remaining ) : 0 ) + "/" + TO_WAIT_FOR_FINISH_LIFECYCLE_ACTION + + " - " + Thread.currentThread().getName()); + System.err.println(" - "+toString()); + } finally { + stateSync.unlock(); + } + if(nok) { + Thread.dumpStack(); + } + } + return !nok; + } + + protected final boolean isStartedImpl() { + return animThread != null ; + } + @Override + public boolean isStarted() { + stateSync.lock(); + try { + return animThread != null ; + } finally { + stateSync.unlock(); + } + } + public String toString() { - return getClass().getName()+"[started "+isStarted()+", animating "+isAnimating()+", paused "+isPaused()+", drawable "+drawables.size()+", totals[dt "+getTotalFPSDuration()+", frames "+getTotalFPSFrames()+", fps "+getTotalFPS()+"]]"; + return getClass().getName()+"[started "+isStarted()+", animating "+isAnimating()+", paused "+isPaused()+", drawable "+drawables.size()+ + ", totals[dt "+getTotalFPSDuration()+", frames "+getTotalFPSFrames()+", fps "+getTotalFPS()+ + "], modeBits "+modeBits+", init'ed "+(null!=impl)+", animThread "+getThread()+", exclCtxThread "+exclusiveContext+"("+getExclusiveContextThread()+")]"; } } diff --git a/src/jogl/classes/com/jogamp/opengl/util/FPSAnimator.java b/src/jogl/classes/com/jogamp/opengl/util/FPSAnimator.java index 251792110..bfcab23fd 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/FPSAnimator.java +++ b/src/jogl/classes/com/jogamp/opengl/util/FPSAnimator.java @@ -39,8 +39,11 @@ */ package com.jogamp.opengl.util; -import java.util.*; -import javax.media.opengl.*; +import java.util.Timer; +import java.util.TimerTask; + +import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLException; /** * An Animator subclass which attempts to achieve a target @@ -54,10 +57,12 @@ import javax.media.opengl.*; */ public class FPSAnimator extends AnimatorBase { private Timer timer = null; - private TimerTask task = null; + private MainTask task = null; private int fps; private boolean scheduleAtFixedRate; - private volatile boolean shouldRun; + private boolean isAnimating; // MainTask feedback + private volatile boolean shouldRun; // MainTask trigger + private volatile boolean shouldStop; // MainTask trigger protected String getBaseName(String prefix) { return "FPS" + prefix + "Animator" ; @@ -88,6 +93,7 @@ public class FPSAnimator extends AnimatorBase { value, an initial drawable to animate, and a flag indicating whether to use fixed-rate scheduling. */ public FPSAnimator(GLAutoDrawable drawable, int fps, boolean scheduleAtFixedRate) { + super(); this.fps = fps; if (drawable != null) { add(drawable); @@ -95,133 +101,260 @@ public class FPSAnimator extends AnimatorBase { this.scheduleAtFixedRate = scheduleAtFixedRate; } - public final boolean isStarted() { - stateSync.lock(); - try { - return (timer != null); - } finally { - stateSync.unlock(); + /** + * @param fps + * @throws GLException if the animator has already been started + */ + public final synchronized void setFPS(int fps) throws GLException { + if ( isStartedImpl() ) { + throw new GLException("Animator already started."); } + this.fps = fps; } + public final int getFPS() { return fps; } + + class MainTask extends TimerTask { + private boolean justStarted; + private boolean alreadyStopped; + private boolean alreadyPaused; + + public MainTask() { + } + + public void start(Timer timer) { + fpsCounter.resetFPSCounter(); + shouldRun = true; + shouldStop = false; + + justStarted = true; + alreadyStopped = false; + alreadyPaused = false; + final long period = 0 < fps ? (long) (1000.0f / (float) fps) : 1; // 0 -> 1: IllegalArgumentException: Non-positive period + if (scheduleAtFixedRate) { + timer.scheduleAtFixedRate(this, 0, period); + } else { + timer.schedule(this, 0, period); + } + } + + public boolean isActive() { return !alreadyStopped && !alreadyPaused; } + + public String toString() { + return "Task[thread "+animThread+", stopped "+alreadyStopped+", paused "+alreadyPaused+" shouldRun "+shouldRun+", shouldStop "+shouldStop+" -- started "+isStartedImpl()+", animating "+isAnimatingImpl()+", paused "+isPausedImpl()+", drawable "+drawables.size()+", drawablesEmpty "+drawablesEmpty+"]"; + } + + public void run() { + if( justStarted ) { + justStarted = false; + synchronized (FPSAnimator.this) { + animThread = Thread.currentThread(); + if(DEBUG) { + System.err.println("FPSAnimator start/resume:" + Thread.currentThread() + ": " + toString()); + } + isAnimating = true; + if( drawablesEmpty ) { + shouldRun = false; // isAnimating:=false @ pause below + } else { + shouldRun = true; + setDrawablesExclCtxState(exclusiveContext); + FPSAnimator.this.notifyAll(); + } + System.err.println("FPSAnimator P1:" + Thread.currentThread() + ": " + toString()); + } + } + if( shouldRun ) { + display(); + } else if( shouldStop ) { // STOP + System.err.println("FPSAnimator P4: "+alreadyStopped+", "+ Thread.currentThread() + ": " + toString()); + this.cancel(); + + if( !alreadyStopped ) { + alreadyStopped = true; + if( exclusiveContext && !drawablesEmpty ) { + setDrawablesExclCtxState(false); + display(); // propagate exclusive change! + } + synchronized (FPSAnimator.this) { + if(DEBUG) { + System.err.println("FPSAnimator stop " + Thread.currentThread() + ": " + toString()); + } + animThread = null; + isAnimating = false; + FPSAnimator.this.notifyAll(); + } + } + } else { + System.err.println("FPSAnimator P5: "+alreadyPaused+", "+ Thread.currentThread() + ": " + toString()); + this.cancel(); + + if( !alreadyPaused ) { // PAUSE + alreadyPaused = true; + if( exclusiveContext && !drawablesEmpty ) { + setDrawablesExclCtxState(false); + display(); // propagate exclusive change! + } + synchronized (FPSAnimator.this) { + if(DEBUG) { + System.err.println("FPSAnimator pause " + Thread.currentThread() + ": " + toString()); + } + isAnimating = false; + FPSAnimator.this.notifyAll(); + } + } + } + } + } + private final boolean isAnimatingImpl() { + return animThread != null && isAnimating ; + } public final boolean isAnimating() { stateSync.lock(); try { - return (timer != null) && (task != null); + return animThread != null && isAnimating ; } finally { stateSync.unlock(); } } + private final boolean isPausedImpl() { + return animThread != null && ( !shouldRun && !shouldStop ) ; + } public final boolean isPaused() { stateSync.lock(); try { - return (timer != null) && (task == null); + return animThread != null && ( !shouldRun && !shouldStop ) ; } finally { stateSync.unlock(); } } - private void startTask() { - if(null != task) { - return; - } - final long period = (long) (1000.0f / (float) fps); - task = new TimerTask() { - public void run() { - if(FPSAnimator.this.shouldRun) { - FPSAnimator.this.animThread = Thread.currentThread(); - // display impl. uses synchronized block on the animator instance - display(); - } - } - }; - - fpsCounter.resetFPSCounter(); - shouldRun = true; - - if (scheduleAtFixedRate) { - timer.scheduleAtFixedRate(task, 0, period); - } else { - timer.schedule(task, 0, period); - } - } - - public synchronized boolean start() { - if (timer != null) { + static int timerNo = 0; + + public synchronized boolean start() { + if ( null != timer || null != task || isStartedImpl() ) { return false; } - stateSync.lock(); - try { - timer = new Timer(); - startTask(); - } finally { - stateSync.unlock(); + timer = new Timer( Thread.currentThread().getName()+"-"+baseName+"-Timer"+(timerNo++) ); + task = new MainTask(); + if(DEBUG) { + System.err.println("FPSAnimator.start() START: "+task+", "+ Thread.currentThread() + ": " + toString()); } - return true; + task.start(timer); + + final boolean res = finishLifecycleAction( drawablesEmpty ? waitForStartedEmptyCondition : waitForStartedAddedCondition, + POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION); + if(DEBUG) { + System.err.println("FPSAnimator.start() END: "+task+", "+ Thread.currentThread() + ": " + toString()); + } + if( drawablesEmpty ) { + task.cancel(); + task = null; + } + return res; } + private final Condition waitForStartedAddedCondition = new Condition() { + public boolean eval() { + return !isStartedImpl() || !isAnimating ; + } }; + private final Condition waitForStartedEmptyCondition = new Condition() { + public boolean eval() { + return !isStartedImpl() || isAnimating ; + } }; /** Stops this FPSAnimator. Due to the implementation of the FPSAnimator it is not guaranteed that the FPSAnimator will be completely stopped by the time this method returns. */ public synchronized boolean stop() { - if (timer == null) { + if ( null == timer || !isStartedImpl() ) { return false; + } + if(DEBUG) { + System.err.println("FPSAnimator.stop() START: "+task+", "+ Thread.currentThread() + ": " + toString()); } - stateSync.lock(); - try { + final boolean res; + if( null == task ) { + // start/resume case w/ drawablesEmpty + res = true; + } else { shouldRun = false; - if(null != task) { - task.cancel(); - task = null; - } - if(null != timer) { - timer.cancel(); - timer = null; - } - animThread = null; - try { - final long periodx2 = 2L * (long) (1000.0f / (float) fps); - Thread.sleep(periodx2 > 20L ? periodx2 : 20L); // max(2 x timer period, ~ 1/60), since we can't ctrl stopped threads - } catch (InterruptedException e) { } - } finally { - stateSync.unlock(); + shouldStop = true; + res = finishLifecycleAction(waitForStoppedCondition, POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION); + } + + if(DEBUG) { + System.err.println("FPSAnimator.stop() END: "+task+", "+ Thread.currentThread() + ": " + toString()); + } + if(null != task) { + task.cancel(); + task = null; } - return true; + if(null != timer) { + timer.cancel(); + timer = null; + } + animThread = null; + return res; } + private final Condition waitForStoppedCondition = new Condition() { + public boolean eval() { + return isStartedImpl(); + } }; public synchronized boolean pause() { - if (timer == null) { + if ( !isStartedImpl() || ( null != task && isPausedImpl() ) ) { return false; } - stateSync.lock(); - try { + if(DEBUG) { + System.err.println("FPSAnimator.pause() START: "+task+", "+ Thread.currentThread() + ": " + toString()); + } + final boolean res; + if( null == task ) { + // start/resume case w/ drawablesEmpty + res = true; + } else { shouldRun = false; - if(null != task) { - task.cancel(); - task = null; - } - animThread = null; - try { - final long periodx2 = 2L * (long) (1000.0f / (float) fps); - Thread.sleep(periodx2 > 20L ? periodx2 : 20L); // max(2 x timer period, ~ 1/60), since we can't ctrl stopped threads - } catch (InterruptedException e) { } - } finally { - stateSync.unlock(); + res = finishLifecycleAction(waitForPausedCondition, POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION); + } + + if(DEBUG) { + System.err.println("FPSAnimator.pause() END: "+task+", "+ Thread.currentThread() + ": " + toString()); + } + if(null != task) { + task.cancel(); + task = null; } - return true; + return res; } + private final Condition waitForPausedCondition = new Condition() { + public boolean eval() { + // end waiting if stopped as well + return isAnimating && isStartedImpl(); + } }; public synchronized boolean resume() { - if (timer == null) { + if ( null != task || !isStartedImpl() || !isPausedImpl() ) { return false; } - stateSync.lock(); - try { - startTask(); - } finally { - stateSync.unlock(); + if(DEBUG) { + System.err.println("FPSAnimator.resume() START: "+ Thread.currentThread() + ": " + toString()); + } + final boolean res; + if( drawablesEmpty ) { + res = true; + } else { + task = new MainTask(); + task.start(timer); + res = finishLifecycleAction(waitForResumeCondition, POLLP_WAIT_FOR_FINISH_LIFECYCLE_ACTION); + } + if(DEBUG) { + System.err.println("FPSAnimator.resume() END: "+task+", "+ Thread.currentThread() + ": " + toString()); } - return true; + return res; } + private final Condition waitForResumeCondition = new Condition() { + public boolean eval() { + // end waiting if stopped as well + return !drawablesEmpty && !isAnimating && isStartedImpl(); + } }; } diff --git a/src/jogl/classes/javax/media/opengl/GLAnimatorControl.java b/src/jogl/classes/javax/media/opengl/GLAnimatorControl.java index 83e9e22c4..3052b924e 100644 --- a/src/jogl/classes/javax/media/opengl/GLAnimatorControl.java +++ b/src/jogl/classes/javax/media/opengl/GLAnimatorControl.java @@ -117,7 +117,7 @@ public interface GLAnimatorControl extends FPSCounter { * or in some cases from an implementation-internal thread like the * AWT event queue thread. * - * @return false if if not started or already paused, otherwise true + * @return false if not started, already paused or failed to pause, otherwise true * * @see #resume() * @see #isAnimating() @@ -134,7 +134,7 @@ public interface GLAnimatorControl extends FPSCounter { * <P> * If resumed, all counters (time, frames, ..) are reset to zero. * - * @return false if if not started or not paused, otherwise true + * @return false if not started, not paused or unable to resume, otherwise true * * @see #pause() * @see #isAnimating() @@ -142,13 +142,24 @@ public interface GLAnimatorControl extends FPSCounter { boolean resume(); /** + * Adds a drawable to this animator's list of rendering drawables.<br> + * This allows the animator thread to become active, i.e. {@link #isAnimating()}==true, + * in case the first drawable is added and {@link #isStarted()} and not {@link #isPaused()}.<br> + * + * @param drawable the drawable to be added + * @throws IllegalArgumentException if drawable was already added to this animator + */ + void add(GLAutoDrawable drawable); + + /** * Removes a drawable from the animator's list of rendering drawables.<br> * This method should get called in case a drawable becomes invalid, * and will not be recovered.<br> - * This allows the animator thread to become idle in case the last drawable - * has reached it's end of life.<br> + * This allows the animator thread to become idle, i.e. {@link #isAnimating()}==false, + * in case the last drawable has reached it's end of life.<br> * - * @param drawable the to be removed drawable + * @param drawable the drawable to be removed + * @throws IllegalArgumentException if drawable was not added to this animator */ void remove(GLAutoDrawable drawable); } diff --git a/src/jogl/classes/javax/media/opengl/GLAutoDrawable.java b/src/jogl/classes/javax/media/opengl/GLAutoDrawable.java index 0f487f463..a7db3f3fd 100644 --- a/src/jogl/classes/javax/media/opengl/GLAutoDrawable.java +++ b/src/jogl/classes/javax/media/opengl/GLAutoDrawable.java @@ -311,13 +311,13 @@ public interface GLAutoDrawable extends GLDrawable { public GLEventListener removeGLEventListener(GLEventListener listener); /** - * <p> * Registers the usage of an animator, an {@link javax.media.opengl.GLAnimatorControl} implementation. - * The animator will be queried whether it's animating, ie periodically issuing {@link #display()} calls or not.</p><br> + * The animator will be queried whether it's animating, ie periodically issuing {@link #display()} calls or not. * <p> * This method shall be called by an animator implementation only,<br> * e.g. {@link com.jogamp.opengl.util.Animator#add(javax.media.opengl.GLAutoDrawable)}, passing it's control implementation,<br> - * and {@link com.jogamp.opengl.util.Animator#remove(javax.media.opengl.GLAutoDrawable)}, passing <code>null</code>.</p><br> + * and {@link com.jogamp.opengl.util.Animator#remove(javax.media.opengl.GLAutoDrawable)}, passing <code>null</code>. + * </p> * <p> * Impacts {@link #display()} and {@link #invoke(boolean, GLRunnable)} semantics.</p><br> * @@ -341,6 +341,45 @@ public interface GLAutoDrawable extends GLDrawable { public GLAnimatorControl getAnimator(); /** + * Dedicates this instance's {@link GLContext} to the given thread.<br/> + * The thread will exclusively claim the {@link GLContext} via {@link #display()} and not release it + * until {@link #destroy()} or <code>setExclusiveContextThread(null)</code> has been called. + * <p> + * Default non-exclusive behavior is <i>requested</i> via <code>setExclusiveContextThread(null)</code>, + * which will cause the next call of {@link #display()} on the exclusive thread to + * release the {@link GLContext}. Only after it's async release, {@link #getExclusiveContextThread()} + * will return <code>null</code>. + * </p> + * <p> + * To release a previous made exclusive thread, a user issues <code>setExclusiveContextThread(null)</code> + * and may poll {@link #getExclusiveContextThread()} until it returns <code>null</code>, + * <i>while</i> the exclusive thread is still running. + * </p> + * <p> + * Note: Setting a new exclusive thread without properly releasing a previous one + * will throw an GLException. + * </p> + * <p> + * Note: Utilizing this feature w/ AWT could lead to an AWT-EDT deadlock, depending on the AWT implementation. + * Hence it is advised not to use it with native AWT GLAutoDrawable like GLCanvas. + * </p> + * <p> + * One scenario could be to dedicate the context to the {@link GLAnimatorControl#getThread() animator thread} + * and spare redundant context switches, see {@link com.jogamp.opengl.util.AnimatorBase#setExclusiveContext(boolean)}. + * </p> + * @param t the exclusive thread to claim the context, or <code>null</code> for default operation. + * @return previous exclusive context thread + * @throws GLException If an exclusive thread is still active but a new one is attempted to be set + * @see com.jogamp.opengl.util.AnimatorBase#setExclusiveContext(boolean) + */ + public Thread setExclusiveContextThread(Thread t) throws GLException; + + /** + * @see #setExclusiveContextThread(Thread) + */ + public Thread getExclusiveContextThread(); + + /** * Enqueues a one-shot {@link GLRunnable}, * which will be executed within the next {@link #display()} call * after all registered {@link GLEventListener}s diff --git a/src/jogl/classes/javax/media/opengl/GLContext.java b/src/jogl/classes/javax/media/opengl/GLContext.java index 235003c38..461d481a8 100644 --- a/src/jogl/classes/javax/media/opengl/GLContext.java +++ b/src/jogl/classes/javax/media/opengl/GLContext.java @@ -276,6 +276,9 @@ public abstract class GLContext { /** * Makes this GLContext current on the calling thread. * <p> + * Recursive call to {@link #makeCurrent()} and hence {@link #release()} are supported. + * </p> + * <p> * There are two return values that indicate success and one that * indicates failure. * </p> @@ -288,7 +291,7 @@ public abstract class GLContext { * </p> * <p> * A return value of {@link #CONTEXT_CURRENT} indicates that the context has - * been made currrent, with its previous state restored. + * been made current, with its previous state restored. * </p> * <p> * If the context could not be made current (for example, because @@ -320,6 +323,9 @@ public abstract class GLContext { /** * Releases control of this GLContext from the current thread. * <p> + * Recursive call to {@link #release()} and hence {@link #makeCurrent()} are supported. + * </p> + * <p> * The drawable's surface is being unlocked at exit, * assumed to be locked by {@link #makeCurrent()}. * </p> diff --git a/src/jogl/classes/javax/media/opengl/awt/GLCanvas.java b/src/jogl/classes/javax/media/opengl/awt/GLCanvas.java index 2f7fef9be..2de86b545 100644 --- a/src/jogl/classes/javax/media/opengl/awt/GLCanvas.java +++ b/src/jogl/classes/javax/media/opengl/awt/GLCanvas.java @@ -742,6 +742,16 @@ public class GLCanvas extends Canvas implements AWTGLAutoDrawable, WindowClosing } @Override + public final Thread setExclusiveContextThread(Thread t) throws GLException { + return helper.setExclusiveContextThread(t, context); + } + + @Override + public final Thread getExclusiveContextThread() { + return helper.getExclusiveContextThread(); + } + + @Override public boolean invoke(boolean wait, GLRunnable glRunnable) { return helper.invoke(this, wait, glRunnable); } diff --git a/src/jogl/classes/javax/media/opengl/awt/GLJPanel.java b/src/jogl/classes/javax/media/opengl/awt/GLJPanel.java index 23dedaa66..664edb996 100644 --- a/src/jogl/classes/javax/media/opengl/awt/GLJPanel.java +++ b/src/jogl/classes/javax/media/opengl/awt/GLJPanel.java @@ -474,6 +474,16 @@ public class GLJPanel extends JPanel implements AWTGLAutoDrawable, WindowClosing } @Override + public final Thread setExclusiveContextThread(Thread t) throws GLException { + return helper.setExclusiveContextThread(t, getContext()); + } + + @Override + public final Thread getExclusiveContextThread() { + return helper.getExclusiveContextThread(); + } + + @Override public boolean invoke(boolean wait, GLRunnable glRunnable) { return helper.invoke(this, wait, glRunnable); } diff --git a/src/jogl/classes/jogamp/opengl/GLAutoDrawableBase.java b/src/jogl/classes/jogamp/opengl/GLAutoDrawableBase.java index cbb7cd699..eadd59559 100644 --- a/src/jogl/classes/jogamp/opengl/GLAutoDrawableBase.java +++ b/src/jogl/classes/jogamp/opengl/GLAutoDrawableBase.java @@ -50,7 +50,6 @@ import javax.media.opengl.GLRunnable; import com.jogamp.common.util.locks.RecursiveLock; import com.jogamp.opengl.GLAutoDrawableDelegate; -import com.jogamp.opengl.util.Animator; /** @@ -407,6 +406,16 @@ public abstract class GLAutoDrawableBase implements GLAutoDrawable, FPSCounter { } @Override + public final Thread setExclusiveContextThread(Thread t) throws GLException { + return helper.setExclusiveContextThread(t, context); + } + + @Override + public final Thread getExclusiveContextThread() { + return helper.getExclusiveContextThread(); + } + + @Override public final boolean invoke(boolean wait, GLRunnable glRunnable) { return helper.invoke(this, wait, glRunnable); } @@ -532,25 +541,6 @@ public abstract class GLAutoDrawableBase implements GLAutoDrawable, FPSCounter { return null != _drawable ? _drawable.getHeight() : 0; } - /** - * @param t the thread for which context release shall be skipped, usually the animation thread, - * ie. {@link Animator#getThread()}. - * @deprecated This is an experimental feature, - * intended for measuring performance in regards to GL context switch. - */ - @Deprecated - public void setSkipContextReleaseThread(Thread t) { - helper.setSkipContextReleaseThread(t); - } - - /** - * @deprecated see {@link #setSkipContextReleaseThread(Thread)} - */ - @Deprecated - public Thread getSkipContextReleaseThread() { - return helper.getSkipContextReleaseThread(); - } - @Override public final GLCapabilitiesImmutable getChosenGLCapabilities() { final GLDrawable _drawable = drawable; diff --git a/src/jogl/classes/jogamp/opengl/GLDrawableHelper.java b/src/jogl/classes/jogamp/opengl/GLDrawableHelper.java index dc5d50cf2..f8c58ee34 100644 --- a/src/jogl/classes/jogamp/opengl/GLDrawableHelper.java +++ b/src/jogl/classes/jogamp/opengl/GLDrawableHelper.java @@ -58,8 +58,6 @@ import javax.media.opengl.GLException; import javax.media.opengl.GLFBODrawable; import javax.media.opengl.GLRunnable; -import com.jogamp.opengl.util.Animator; - /** Encapsulates the implementation of most of the GLAutoDrawable's methods to be able to share it between GLCanvas and GLJPanel. */ public class GLDrawableHelper { @@ -73,7 +71,9 @@ public class GLDrawableHelper { private final Object glRunnablesLock = new Object(); private volatile ArrayList<GLRunnableTask> glRunnables = new ArrayList<GLRunnableTask>(); private boolean autoSwapBufferMode; - private Thread skipContextReleaseThread; + private volatile Thread exclusiveContextThread; + /** -1 release, 0 nop, 1 claim */ + private volatile int exclusiveContextSwitch; private GLAnimatorControl animatorCtrl; private static Runnable nop = new Runnable() { public void run() {} }; @@ -87,7 +87,8 @@ public class GLDrawableHelper { listenersToBeInit.clear(); } autoSwapBufferMode = true; - skipContextReleaseThread = null; + exclusiveContextThread = null; + exclusiveContextSwitch = 0; synchronized(glRunnablesLock) { glRunnables.clear(); } @@ -113,6 +114,23 @@ public class GLDrawableHelper { } /** + * Since GLContext's {@link GLContext#makeCurrent()} and {@link GLContext#release()} + * is recursive, a call to {@link GLContext#release()} may not natively release the context. + * <p> + * This methods continues calling {@link GLContext#release()} until the context has been natively released. + * </p> + * @param ctx + */ + public static final void forceNativeRelease(GLContext ctx) { + do { + ctx.release(); + if (DEBUG) { + System.err.println("GLDrawableHelper.forceNativeRelease() -- currentThread "+Thread.currentThread()+" -> "+GLContext.getCurrent()); + } + } while( ctx == GLContext.getCurrent() ); + } + + /** * Associate a new context to the drawable and also propagates the context/drawable switch by * calling {@link GLContext#setGLDrawable(GLDrawable, boolean) newCtx.setGLDrawable(drawable, true);}. * <p> @@ -767,23 +785,72 @@ public class GLDrawableHelper { return autoSwapBufferMode; } + private final String getExclusiveContextSwitchString() { + return 0 == exclusiveContextSwitch ? "nop" : ( 0 > exclusiveContextSwitch ? "released" : "claimed" ) ; + } + /** - * @param t the thread for which context release shall be skipped, usually the animation thread, - * ie. {@link Animator#getThread()}. - * @deprecated this is an experimental feature, - * intended for measuring performance in regards to GL context switch - * and only being used if {@link #PERF_STATS} is enabled - * by defining property <code>jogl.debug.GLDrawable.PerfStats</code>. + * Dedicates this instance's {@link GLContext} to the given thread.<br/> + * The thread will exclusively claim the {@link GLContext} via {@link #display()} and not release it + * until {@link #destroy()} or <code>setExclusiveContextThread(null)</code> has been called. + * <p> + * Default non-exclusive behavior is <i>requested</i> via <code>setExclusiveContextThread(null)</code>, + * which will cause the next call of {@link #display()} on the exclusive thread to + * release the {@link GLContext}. Only after it's async release, {@link #getExclusiveContextThread()} + * will return <code>null</code>. + * </p> + * <p> + * To release a previous made exclusive thread, a user issues <code>setExclusiveContextThread(null)</code> + * and may poll {@link #getExclusiveContextThread()} until it returns <code>null</code>, + * <i>while</i> the exclusive thread is still running. + * </p> + * <p> + * Note: Setting a new exclusive thread without properly releasing a previous one + * will throw an GLException. + * </p> + * <p> + * One scenario could be to dedicate the context to the {@link com.jogamp.opengl.util.AnimatorBase#getThread() animator thread} + * and spare redundant context switches. + * </p> + * @param t the exclusive thread to claim the context, or <code>null</code> for default operation. + * @return previous exclusive context thread + * @throws GLException If an exclusive thread is still active but a new one is attempted to be set */ - public final void setSkipContextReleaseThread(Thread t) { - skipContextReleaseThread = t; + public final Thread setExclusiveContextThread(Thread t, GLContext context) throws GLException { + if (DEBUG) { + System.err.println("GLDrawableHelper.setExclusiveContextThread(): START switch "+getExclusiveContextSwitchString()+", thread "+exclusiveContextThread+" -> "+t+" -- currentThread "+Thread.currentThread()); + } + final Thread oldExclusiveContextThread = exclusiveContextThread; + if( exclusiveContextThread == t ) { + exclusiveContextSwitch = 0; // keep + } else if( null == t ) { + exclusiveContextSwitch = -1; // release + } else { + exclusiveContextSwitch = 1; // claim + if( null != exclusiveContextThread ) { + throw new GLException("Release current exclusive Context Thread "+exclusiveContextThread+" first"); + } + if( null != context && GLContext.getCurrent() == context ) { + try { + forceNativeRelease(context); + } catch (Throwable ex) { + ex.printStackTrace(); + throw new GLException(ex); + } + } + exclusiveContextThread = t; + } + if (DEBUG) { + System.err.println("GLDrawableHelper.setExclusiveContextThread(): END switch "+getExclusiveContextSwitchString()+", thread "+exclusiveContextThread+" -- currentThread "+Thread.currentThread()); + } + return oldExclusiveContextThread; } - + /** - * @deprecated see {@link #setSkipContextReleaseThread(Thread)} + * @see #setExclusiveContextThread(Thread, GLContext) */ - public final Thread getSkipContextReleaseThread() { - return skipContextReleaseThread; + public final Thread getExclusiveContextThread() { + return exclusiveContextThread; } private static final ThreadLocal<Runnable> perThreadInitAction = new ThreadLocal<Runnable>(); @@ -828,7 +895,10 @@ public class GLDrawableHelper { * {@link #disposeAllGLEventListener(GLAutoDrawable, boolean) disposeAllGLEventListener(autoDrawable, false)} * with the context made current. * <p> - * If <code>destroyContext</code> is <code>true</code> the context is destroyed in the end while holding the lock.<br/> + * If <code>destroyContext</code> is <code>true</code> the context is destroyed in the end while holding the lock. + * </p> + * <p> + * If <code>destroyContext</code> is <code>false</code> the context is natively released, i.e. released as often as locked before. * </p> * @param autoDrawable * @param context @@ -842,14 +912,15 @@ public class GLDrawableHelper { Runnable lastInitAction = null; if (lastContext != null) { if (lastContext == context) { - lastContext = null; // utilize recursive locking + lastContext = null; } else { + // utilize recursive locking lastInitAction = perThreadInitAction.get(); lastContext.release(); } } - int res = GLContext.CONTEXT_NOT_CURRENT; - + + int res; try { res = context.makeCurrent(); if (GLContext.CONTEXT_NOT_CURRENT != res) { @@ -865,7 +936,7 @@ public class GLDrawableHelper { if(destroyContext) { context.destroy(); } else { - context.release(); + forceNativeRelease(context); } flushGLRunnables(); } catch (Exception e) { @@ -880,136 +951,204 @@ public class GLDrawableHelper { } } } - + private final void invokeGLImpl(final GLDrawable drawable, - final GLContext context, - final Runnable runnable, - final Runnable initAction) { - // Support for recursive makeCurrent() calls as well as calling - // other drawables' display() methods from within another one's - GLContext lastContext = GLContext.getCurrent(); - Runnable lastInitAction = null; - if (lastContext != null) { - if (lastContext == context) { - lastContext = null; // utilize recursive locking - } else { - lastInitAction = perThreadInitAction.get(); - lastContext.release(); - } - } - int res = GLContext.CONTEXT_NOT_CURRENT; - - try { - res = context.makeCurrent(); - if (GLContext.CONTEXT_NOT_CURRENT != res) { - try { - perThreadInitAction.set(initAction); - if (GLContext.CONTEXT_CURRENT_NEW == res) { - if (DEBUG) { - System.err.println("GLDrawableHelper " + this + ".invokeGL(): Running initAction"); - } - initAction.run(); - } - runnable.run(); - if ( autoSwapBufferMode ) { - drawable.swapBuffers(); - } - } finally { - try { - context.release(); - } catch (Exception e) { - System.err.println("Catched: "+e.getMessage()); - e.printStackTrace(); - } - } - } - } finally { - if (lastContext != null) { - final int res2 = lastContext.makeCurrent(); - if (null != lastInitAction && res2 == GLContext.CONTEXT_CURRENT_NEW) { - lastInitAction.run(); - } - } - } - } - - private final void invokeGLImplStats(final GLDrawable drawable, - final GLContext context, - final Runnable runnable, - final Runnable initAction) { - final Thread currentThread = Thread.currentThread(); - - // Support for recursive makeCurrent() calls as well as calling - // other drawables' display() methods from within another one's - int res = GLContext.CONTEXT_NOT_CURRENT; - GLContext lastContext = GLContext.getCurrent(); - Runnable lastInitAction = null; - if (lastContext != null) { - if (lastContext == context) { - if( currentThread == skipContextReleaseThread ) { - res = GLContext.CONTEXT_CURRENT; - } // else: utilize recursive locking - lastContext = null; - } else { - lastInitAction = perThreadInitAction.get(); - lastContext.release(); - } - } - - long t0 = System.currentTimeMillis(); - long tdA = 0; // makeCurrent - long tdR = 0; // render time - long tdS = 0; // swapBuffers - long tdX = 0; // release - boolean ctxClaimed = false; - boolean ctxReleased = false; - boolean ctxDestroyed = false; - try { - if (res == GLContext.CONTEXT_NOT_CURRENT) { - res = context.makeCurrent(); - ctxClaimed = true; + final GLContext context, + final Runnable runnable, + final Runnable initAction) { + final Thread currentThread = Thread.currentThread(); + + // Exclusive Cases: + // 1: lock - unlock : default + // 2: lock - - : exclusive, not locked yet + // 3: - - - : exclusive, already locked + // 4: - - unlock : ex-exclusive, already locked + final boolean _isExclusiveThread, _releaseExclusiveThread; + if( null != exclusiveContextThread) { + if( currentThread == exclusiveContextThread ) { + _releaseExclusiveThread = 0 > exclusiveContextSwitch; + _isExclusiveThread = !_releaseExclusiveThread; + exclusiveContextSwitch = 0; + } else { + // Exclusive thread usage, but on other thread + return; + } + } else { + _releaseExclusiveThread = false; + _isExclusiveThread = false; } - if (res != GLContext.CONTEXT_NOT_CURRENT) { - perThreadInitAction.set(initAction); - if (res == GLContext.CONTEXT_CURRENT_NEW) { - if (DEBUG) { - System.err.println("GLDrawableHelper " + this + ".invokeGL(): Running initAction"); + + // Support for recursive makeCurrent() calls as well as calling + // other drawables' display() methods from within another one's + int res = GLContext.CONTEXT_NOT_CURRENT; + GLContext lastContext = GLContext.getCurrent(); + Runnable lastInitAction = null; + if (lastContext != null) { + if (lastContext == context) { + res = GLContext.CONTEXT_CURRENT; + lastContext = null; + } else { + // utilize recursive locking + lastInitAction = perThreadInitAction.get(); + lastContext.release(); } - initAction.run(); - } - tdR = System.currentTimeMillis(); - tdA = tdR - t0; // makeCurrent - runnable.run(); - tdS = System.currentTimeMillis(); - tdR = tdS - tdR; // render time - if (autoSwapBufferMode) { - drawable.swapBuffers(); - tdX = System.currentTimeMillis(); - tdS = tdX - tdS; // swapBuffers - } } - } finally { + try { - if( res != GLContext.CONTEXT_NOT_CURRENT && - (null == skipContextReleaseThread || currentThread != skipContextReleaseThread) ) { - context.release(); - ctxReleased = true; + final boolean releaseContext; + if( GLContext.CONTEXT_NOT_CURRENT == res ) { + res = context.makeCurrent(); + releaseContext = !_isExclusiveThread; + } else { + releaseContext = _releaseExclusiveThread; } - } catch (Exception e) { - System.err.println("Catched: "+e.getMessage()); - e.printStackTrace(); + if (GLContext.CONTEXT_NOT_CURRENT != res) { + try { + perThreadInitAction.set(initAction); + if (GLContext.CONTEXT_CURRENT_NEW == res) { + if (DEBUG) { + System.err.println("GLDrawableHelper " + this + ".invokeGL(): Running initAction"); + } + initAction.run(); + } + runnable.run(); + if ( autoSwapBufferMode ) { + drawable.swapBuffers(); + } + } finally { + if( _releaseExclusiveThread ) { + exclusiveContextThread = null; + if (DEBUG) { + System.err.println("GLDrawableHelper.invokeGL() - Release ExclusiveContextThread -- currentThread "+Thread.currentThread()); + } + } + if( releaseContext ) { + try { + context.release(); + } catch (Exception e) { + System.err.println("Catched: "+e.getMessage()); + e.printStackTrace(); + } + } + } + } + } finally { + if (lastContext != null) { + final int res2 = lastContext.makeCurrent(); + if (null != lastInitAction && res2 == GLContext.CONTEXT_CURRENT_NEW) { + lastInitAction.run(); + } + } + } + } + + private final void invokeGLImplStats(final GLDrawable drawable, + final GLContext context, + final Runnable runnable, + final Runnable initAction) { + final Thread currentThread = Thread.currentThread(); + + // Exclusive Cases: + // 1: lock - unlock : default + // 2: lock - - : exclusive, not locked yet + // 3: - - - : exclusive, already locked + // 4: - - unlock : ex-exclusive, already locked + final boolean _isExclusiveThread, _releaseExclusiveThread; + if( null != exclusiveContextThread) { + if( currentThread == exclusiveContextThread ) { + _releaseExclusiveThread = 0 > exclusiveContextSwitch; + _isExclusiveThread = !_releaseExclusiveThread; + } else { + // Exclusive thread usage, but on other thread + return; + } + } else { + _releaseExclusiveThread = false; + _isExclusiveThread = false; } - tdX = System.currentTimeMillis() - tdX; // release / destroy + // Support for recursive makeCurrent() calls as well as calling + // other drawables' display() methods from within another one's + int res = GLContext.CONTEXT_NOT_CURRENT; + GLContext lastContext = GLContext.getCurrent(); + Runnable lastInitAction = null; if (lastContext != null) { - final int res2 = lastContext.makeCurrent(); - if (null != lastInitAction && res2 == GLContext.CONTEXT_CURRENT_NEW) { - lastInitAction.run(); - } + if (lastContext == context) { + res = GLContext.CONTEXT_CURRENT; + lastContext = null; + } else { + // utilize recursive locking + lastInitAction = perThreadInitAction.get(); + lastContext.release(); + } } - } - long td = System.currentTimeMillis() - t0; - System.err.println("td0 "+td+"ms, fps "+(1.0/(td/1000.0))+", td-makeCurrent: "+tdA+"ms, td-render "+tdR+"ms, td-swap "+tdS+"ms, td-release "+tdX+"ms, ctx claimed: "+ctxClaimed+", ctx release: "+ctxReleased+", ctx destroyed "+ctxDestroyed); + + long t0 = System.currentTimeMillis(); + long tdA = 0; // makeCurrent + long tdR = 0; // render time + long tdS = 0; // swapBuffers + long tdX = 0; // release + boolean ctxClaimed = false; + boolean ctxReleased = false; + boolean ctxDestroyed = false; + try { + final boolean releaseContext; + if( GLContext.CONTEXT_NOT_CURRENT == res ) { + res = context.makeCurrent(); + releaseContext = !_isExclusiveThread; + ctxClaimed = true; + } else { + releaseContext = _releaseExclusiveThread; + } + if (GLContext.CONTEXT_NOT_CURRENT != res) { + try { + perThreadInitAction.set(initAction); + if (GLContext.CONTEXT_CURRENT_NEW == res) { + if (DEBUG) { + System.err.println("GLDrawableHelper " + this + ".invokeGL(): Running initAction"); + } + initAction.run(); + } + tdR = System.currentTimeMillis(); + tdA = tdR - t0; // makeCurrent + runnable.run(); + tdS = System.currentTimeMillis(); + tdR = tdS - tdR; // render time + if ( autoSwapBufferMode ) { + drawable.swapBuffers(); + tdX = System.currentTimeMillis(); + tdS = tdX - tdS; // swapBuffers + } + } finally { + if( _releaseExclusiveThread ) { + exclusiveContextSwitch = 0; + exclusiveContextThread = null; + if (DEBUG) { + System.err.println("GLDrawableHelper.invokeGL() - Release ExclusiveContextThread -- currentThread "+Thread.currentThread()); + } + } + if( releaseContext ) { + try { + context.release(); + ctxReleased = true; + } catch (Exception e) { + System.err.println("Catched: "+e.getMessage()); + e.printStackTrace(); + } + } + } + } + } finally { + tdX = System.currentTimeMillis() - tdX; // release / destroy + if (lastContext != null) { + final int res2 = lastContext.makeCurrent(); + if (null != lastInitAction && res2 == GLContext.CONTEXT_CURRENT_NEW) { + lastInitAction.run(); + } + } + } + long td = System.currentTimeMillis() - t0; + System.err.println("td0 "+td+"ms, fps "+(1.0/(td/1000.0))+", td-makeCurrent: "+tdA+"ms, td-render "+tdR+"ms, td-swap "+tdS+"ms, td-release "+tdX+"ms, ctx claimed: "+ctxClaimed+", ctx release: "+ctxReleased+", ctx destroyed "+ctxDestroyed); } } |