/** * Copyright 2010 JogAmp Community. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of JogAmp Community. */ package com.jogamp.opengl.util; import com.jogamp.common.util.locks.LockFactory; import com.jogamp.common.util.locks.RecursiveLock; import jogamp.opengl.Debug; import jogamp.opengl.FPSCounterImpl; import java.io.PrintStream; import java.util.ArrayList; import javax.media.opengl.GLAnimatorControl; import javax.media.opengl.GLAutoDrawable; import javax.media.opengl.GLException; import javax.media.opengl.GLProfile; /** * Base implementation of GLAnimatorControl
*

* The change synchronization is done via synchronized blocks on the AnimatorBase instance.
* Status get / set activity is synced with a RecursiveLock, used as a memory barrier.
* This is suitable, since all change requests are allowed to be expensive * as they are not expected to be called at every frame. *

*/ public abstract class AnimatorBase implements GLAnimatorControl { protected static final boolean DEBUG = Debug.debug("Animator"); /** 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 modeBits field and * {@link GLProfile#isAWTAvailable() AWT is available}, * implementation is aware of the AWT EDT, otherwise not. *

* This is the default. *

* @see #setModeBits(boolean, int) */ public static final int MODE_EXPECT_AWT_RENDERING_THREAD = 1 << 0; public interface AnimatorImpl { void display(ArrayList drawables, boolean ignoreExceptions, boolean printExceptions); boolean blockUntilDone(Thread thread); } protected int modeBits; protected AnimatorImpl impl; protected String baseName; protected ArrayList drawables = new ArrayList(); 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(); private final static Class awtAnimatorImplClazz; static { GLProfile.initSingleton(); if( GLProfile.isAWTAvailable() ) { Class clazz; try { 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()}, .. *

* Operation is a NOP if force is false * and this instance is already initialized. *

* * @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); /** * Enables or disables the given bitValues * in this Animators modeBits. * @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: 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); pause(); if( isStarted() ) { drawable.setExclusiveContextThread( exclusiveContext ? getExclusiveContextThread() : null ); // if already running .. } drawables.add(drawable); drawablesEmpty = drawables.size() == 0; drawable.setAnimator(this); if( isPaused() ) { // either paused by pause() above, or if previously drawablesEmpty==true resume(); } 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(); } @Override public synchronized void remove(final GLAutoDrawable drawable) { if(DEBUG) { 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(); drawables.remove(drawable); drawablesEmpty = drawables.size() == 0; drawable.setAnimator(null); if(paused) { resume(); } 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. *

* The given thread will be exclusive to all {@link GLAutoDrawable}'s context while {@link #isAnimating()}. *

*

* 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}. *

*

* 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. *

* * @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. *

* The given thread will be exclusive to all {@link GLAutoDrawable}'s context while {@link #isAnimating()}. *

*

* 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}. *

*

* 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. *

* * @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( 0true, if the exclusive context thread is enabled, otherwise false. * * @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 null. *

* If exclusive context is enabled via {@link #setExclusiveContext(boolean)} * the {@link #getThread() animator thread} is returned if above conditions are met. *

*

* If exclusive context is enabled via {@link #setExclusiveContext(Thread)} * the user passed thread is returned if above conditions are met. *

* @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. *

* At {@link #stop()} an additional {@link #display()} call shall be issued * to allow propagation of releasing the exclusive thread. *

*/ 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; ifalse. * @param pollPeriod if 0, method will wait until TO is reached or being notified. * if > 0, method will wait for the given pollPeriod in milliseconds. * @return true if {@link Condition#eval() waitCondition.eval()} returned false, otherwise false. */ 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 || blocking && nok) { // Info only if DEBUG or ( blocking && not-ok ) ; !blocking possible if AWT 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()+ "], modeBits "+modeBits+", init'ed "+(null!=impl)+", animThread "+getThread()+", exclCtxThread "+exclusiveContext+"("+getExclusiveContextThread()+")]"; } }