/*
* Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
* Copyright (c) 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:
*
* - Redistribution of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistribution 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.
*
* Neither the name of Sun Microsystems, Inc. or the names of
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
* INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
* MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
* ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
* ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
* DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
* DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
* ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
* SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed or intended for use
* in the design, construction, operation or maintenance of any nuclear
* facility.
*
* Sun gratefully acknowledges that this software was originally authored
* and developed by Kenneth Bradley Russell and Christopher John Kline.
*/
package jogamp.opengl;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import javax.media.nativewindow.NativeSurface;
import javax.media.nativewindow.NativeWindowException;
import javax.media.nativewindow.ProxySurface;
import javax.media.nativewindow.UpstreamSurfaceHook;
import javax.media.opengl.GL;
import javax.media.opengl.GLAnimatorControl;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLDrawable;
import javax.media.opengl.GLDrawableFactory;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.GLFBODrawable;
import javax.media.opengl.GLRunnable;
import com.jogamp.common.util.PropertyAccess;
/** Encapsulates the implementation of most of the GLAutoDrawable's
methods to be able to share it between GLAutoDrawable implementations like GLAutoDrawableBase, GLCanvas and GLJPanel. */
public class GLDrawableHelper {
/** true if property
* This methods continues calling {@link GLContext#release()} until the context has been natively released.
*
* Remarks:
* jogl.debug.GLDrawable.PerfStats
is defined. */
private static final boolean PERF_STATS;
static {
Debug.initSingleton();
PERF_STATS = PropertyAccess.isPropertyDefined("jogl.debug.GLDrawable.PerfStats", true);
}
protected static final boolean DEBUG = GLDrawableImpl.DEBUG;
private final Object listenersLock = new Object();
private final ArrayList
*
* oldCtx
will be destroyed if destroyPrevCtx
is true
,
* otherwise disassociate oldCtx
from drawable
* via {@link GLContext#setGLDrawable(GLDrawable, boolean) oldCtx.setGLDrawable(null, true);} including {@link GL#glFinish() glFinish()}.newCtx
with drawable
* via {@link GLContext#setGLDrawable(GLDrawable, boolean) newCtx.setGLDrawable(drawable, true);}.
null
.
* @param destroyOldCtx if true
, destroy the oldCtx
* @param newCtx the new context, maybe null
for dis-association.
* @param newCtxCreationFlags additional creation flags if newCtx is not null and not been created yet, see {@link GLContext#setContextCreationFlags(int)}
*
* @see GLAutoDrawable#setContext(GLContext, boolean)
*/
public static final void switchContext(final GLDrawable drawable, final GLContext oldCtx, final boolean destroyOldCtx, final GLContext newCtx, final int newCtxCreationFlags) {
if( null != oldCtx ) {
if( destroyOldCtx ) {
oldCtx.destroy();
} else {
oldCtx.setGLDrawable(null, true); // dis-associate old pair
}
}
if(null!=newCtx) {
newCtx.setContextCreationFlags(newCtxCreationFlags);
newCtx.setGLDrawable(drawable, true); // re-associate new pair
}
}
/**
* If the drawable is not realized, OP is a NOP.
*
* Locking is performed via {@link GLContext#makeCurrent()} on the passed context
.
*
* The {@link GLDrawableImpl}'s {@link NativeSurface} is being locked during operation. * In case the holder is an auto drawable or similar, it's lock shall be claimed by the caller. *
** May recreate the drawable via {@link #recreateGLDrawable(GLDrawableImpl, GLContext)} * in case of a a pbuffer- or pixmap-drawable. *
** FBO drawables are resized w/o drawable destruction. *
** Offscreen resize operation is validated w/ drawable size in the end. * An exception is thrown if not successful. *
* * @param drawable * @param context * @param newWidth the new width, it's minimum is capped to 1 * @param newHeight the new height, it's minimum is capped to 1 * @return the new drawable in case of an pbuffer/pixmap drawable, otherwise the passed drawable is being returned. * @throws NativeWindowException is drawable is not offscreen or it's surface lock couldn't be claimed * @throws GLException may be thrown a resize operation */ public static final GLDrawableImpl resizeOffscreenDrawable(GLDrawableImpl drawable, final GLContext context, int newWidth, int newHeight) throws NativeWindowException, GLException { final NativeSurface ns = drawable.getNativeSurface(); final int lockRes = ns.lockSurface(); if ( NativeSurface.LOCK_SURFACE_NOT_READY >= lockRes ) { throw new NativeWindowException("Could not lock surface of drawable: "+drawable); } boolean validateSize = true; try { if( ! drawable.isRealized() ) { return drawable; } if( drawable.getChosenGLCapabilities().isOnscreen() ) { throw new NativeWindowException("Drawable is not offscreen: "+drawable); } if( DEBUG && ( 0>=newWidth || 0>=newHeight) ) { System.err.println("WARNING: Odd size detected: "+newWidth+"x"+newHeight+", using safe size 1x1. Drawable "+drawable); Thread.dumpStack(); } if( 0 >= newWidth ) { newWidth = 1; validateSize=false; } if( 0 >= newHeight ) { newHeight = 1; validateSize=false; } // propagate new size if( ns instanceof ProxySurface ) { final ProxySurface ps = (ProxySurface) ns; final UpstreamSurfaceHook ush = ps.getUpstreamSurfaceHook(); if(ush instanceof UpstreamSurfaceHook.MutableSize) { ((UpstreamSurfaceHook.MutableSize)ush).setSurfaceSize(newWidth, newHeight); } else if(DEBUG) { // we have to assume UpstreamSurfaceHook contains the new size already, hence size check @ bottom System.err.println("GLDrawableHelper.resizeOffscreenDrawable: Drawable's offscreen ProxySurface n.a. UpstreamSurfaceHook.MutableSize, but "+ush.getClass().getName()+": "+ush); } } else if(DEBUG) { // we have to assume surface contains the new size already, hence size check @ bottom System.err.println("GLDrawableHelper.resizeOffscreenDrawable: Drawable's offscreen surface n.a. ProxySurface, but "+ns.getClass().getName()+": "+ns); } if( drawable instanceof GLFBODrawable ) { if( null != context && context.isCreated() ) { ((GLFBODrawable) drawable).resetSize(context.getGL()); } } else { drawable = GLDrawableHelper.recreateGLDrawable(drawable, context); } } finally { ns.unlockSurface(); } if( validateSize && ( drawable.getSurfaceWidth() != newWidth || drawable.getSurfaceHeight() != newHeight ) ) { throw new InternalError("Incomplete resize operation: expected "+newWidth+"x"+newHeight+", has: "+drawable); } return drawable; } public final void addGLEventListener(final GLEventListener listener) { addGLEventListener(-1, listener); } public final void addGLEventListener(int index, final GLEventListener listener) { synchronized(listenersLock) { if(0>index) { index = listeners.size(); } // GLEventListener may be added after context is created, // hence we earmark initialization for the next display call. listenersToBeInit.add(listener); listeners.add(index, listener); } } /** * Note that no {@link GLEventListener#dispose(GLAutoDrawable)} call is being issued * due to the lack of a current context. * Consider calling {@link #disposeGLEventListener(GLAutoDrawable, GLDrawable, GLContext, GLEventListener)}. * @return the removed listener, or null if listener was not added */ public final GLEventListener removeGLEventListener(final GLEventListener listener) { synchronized(listenersLock) { listenersToBeInit.remove(listener); return listeners.remove(listener) ? listener : null; } } public final GLEventListener removeGLEventListener(int index) throws IndexOutOfBoundsException { synchronized(listenersLock) { if(0>index) { index = listeners.size()-1; } final GLEventListener listener = listeners.remove(index); listenersToBeInit.remove(listener); return listener; } } public final int getGLEventListenerCount() { synchronized(listenersLock) { return listeners.size(); } } public final GLEventListener getGLEventListener(int index) throws IndexOutOfBoundsException { synchronized(listenersLock) { if(0>index) { index = listeners.size()-1; } return listeners.get(index); } } public final boolean areAllGLEventListenerInitialized() { synchronized(listenersLock) { return 0 == listenersToBeInit.size(); } } public final boolean getGLEventListenerInitState(final GLEventListener listener) { synchronized(listenersLock) { return !listenersToBeInit.contains(listener); } } public final void setGLEventListenerInitState(final GLEventListener listener, final boolean initialized) { synchronized(listenersLock) { if(initialized) { listenersToBeInit.remove(listener); } else { listenersToBeInit.add(listener); } } } /** * Disposes the given {@link GLEventListener} via {@link GLEventListener#dispose(GLAutoDrawable)} * if it has been initialized and added to this queue. *
* If remove
is true
, the {@link GLEventListener} is removed from this drawable queue before disposal,
* otherwise marked uninitialized.
*
* Please consider using {@link #disposeGLEventListener(GLAutoDrawable, GLDrawable, GLContext, GLEventListener)} * for correctness, i.e. encapsulating all calls w/ makeCurrent etc. *
* @param autoDrawable * @param remove if true, the listener gets removed * @return the disposed and/or removed listener, otherwise null if neither action is performed */ public final GLEventListener disposeGLEventListener(final GLAutoDrawable autoDrawable, final GLEventListener listener, final boolean remove) { synchronized(listenersLock) { if( remove ) { if( listeners.remove(listener) ) { if( !listenersToBeInit.remove(listener) ) { listener.dispose(autoDrawable); } return listener; } } else { if( listeners.contains(listener) && !listenersToBeInit.contains(listener) ) { listener.dispose(autoDrawable); listenersToBeInit.add(listener); return listener; } } } return null; } /** * Disposes all added initialized {@link GLEventListener}s via {@link GLEventListener#dispose(GLAutoDrawable)}. *
* If remove
is true
, the {@link GLEventListener}s are removed from this drawable queue before disposal,
* otherwise maked uninitialized.
*
* Please consider using {@link #disposeAllGLEventListener(GLAutoDrawable, GLContext, boolean)} * or {@link #disposeGL(GLAutoDrawable, GLContext, boolean)} * for correctness, i.e. encapsulating all calls w/ makeCurrent etc. *
* @param autoDrawable * @return the disposal count * @throws GLException caused by {@link GLEventListener#dispose(GLAutoDrawable)} */ public final int disposeAllGLEventListener(final GLAutoDrawable autoDrawable, final boolean remove) throws GLException { Throwable firstCaught = null; int disposeCount = 0; synchronized(listenersLock) { if( remove ) { for (int count = listeners.size(); 0 < count && 0 < listeners.size(); count--) { final GLEventListener listener = listeners.remove(0); if( !listenersToBeInit.remove(listener) ) { try { listener.dispose(autoDrawable); } catch (final Throwable t) { if( null == firstCaught ) { firstCaught = t; } else { GLException.dumpThrowable("subsequent", t); } } disposeCount++; } } } else { for (int i = 0; i < listeners.size(); i++) { final GLEventListener listener = listeners.get(i); if( !listenersToBeInit.contains(listener) ) { try { listener.dispose(autoDrawable); } catch (final Throwable t) { if( null == firstCaught ) { firstCaught = t; } else { GLException.dumpThrowable("subsequent", t); } } listenersToBeInit.add(listener); disposeCount++; } } } } if( null != firstCaught ) { flushGLRunnables(); throw GLException.newGLException(firstCaught); } return disposeCount; } /** * Principal helper method which runs {@link #disposeGLEventListener(GLAutoDrawable, GLEventListener, boolean)} * with the context made current. ** If an {@link GLAnimatorControl} is being attached and the current thread is different * than {@link GLAnimatorControl#getThread() the animator's thread}, it is paused during the operation. *
* * @param autoDrawable * @param context * @param listener * @param initAction */ public final GLEventListener disposeGLEventListener(final GLAutoDrawable autoDrawable, final GLDrawable drawable, final GLContext context, final GLEventListener listener, final boolean remove) { synchronized(listenersLock) { // fast path for uninitialized listener if( listenersToBeInit.contains(listener) ) { if( remove ) { listenersToBeInit.remove(listener); return listeners.remove(listener) ? listener : null; } return null; } } final boolean isPaused = isAnimatorAnimatingOnOtherThread() && animatorCtrl.pause(); final GLEventListener[] res = new GLEventListener[] { null }; final Runnable action = new Runnable() { @Override public void run() { res[0] = disposeGLEventListener(autoDrawable, listener, remove); } }; invokeGL(drawable, context, action, nop); if(isPaused) { animatorCtrl.resume(); } return res[0]; } /** * Principal helper method which runs {@link #disposeAllGLEventListener(GLAutoDrawable, boolean)} * with the context made current. ** If an {@link GLAnimatorControl} is being attached and the current thread is different * than {@link GLAnimatorControl#getThread() the animator's thread}, it is paused during the operation. *
* * @param autoDrawable * @param context * @param remove */ public final void disposeAllGLEventListener(final GLAutoDrawable autoDrawable, final GLDrawable drawable, final GLContext context, final boolean remove) { final boolean isPaused = isAnimatorAnimatingOnOtherThread() && animatorCtrl.pause(); final Runnable action = new Runnable() { @Override public void run() { disposeAllGLEventListener(autoDrawable, remove); } }; invokeGL(drawable, context, action, nop); if(isPaused) { animatorCtrl.resume(); } } private final void init(final GLEventListener l, final GLAutoDrawable drawable, final boolean sendReshape, final boolean setViewport) { l.init(drawable); if(sendReshape) { reshape(l, drawable, 0, 0, drawable.getSurfaceWidth(), drawable.getSurfaceHeight(), setViewport, false /* checkInit */); } } /** * The default init action to be called once after ctx is being created @ 1st makeCurrent(). * @param sendReshape set to true if the subsequent display call won't reshape, otherwise false to avoid double reshape. **/ public final void init(final GLAutoDrawable drawable, final boolean sendReshape) { synchronized(listenersLock) { final ArrayList
* If wait
is true
the call blocks until the glRunnable
* has been executed.
*
* If wait
is true
and
* {@link GLDrawable#isRealized()} returns false
or {@link GLAutoDrawable#getContext()} returns null
,
* the call is ignored and returns false
.
* This helps avoiding deadlocking the caller.
*
*
* 0 == deferredHere && 0 == isGLThread -> display() will issue on GL thread, blocking! * * deferredHere wait isGLThread lockedByThisThread Note * OK 0 x 1 x * OK 0 x 0 0 * ERROR 0 x 0 1 Will be deferred on GL thread by display() (blocking), * but locked by this thread -> ERROR * * 1 0 x x All good, due to no wait, non blocking * * 1 1 1 0 * 1 1 0 0 * SWITCH 1 1 1 1 Run immediately, don't defer since locked by this thread, but isGLThread * ERROR 1 1 0 1 Locked by this thread, but _not_ isGLThread -> ERROR ** * * @param drawable the {@link GLAutoDrawable} to be used * @param wait if
true
block until execution of glRunnable
is finished, otherwise return immediatly w/o waiting
* @param glRunnable the {@link GLRunnable} to execute within {@link #display()}
* @return true
if the {@link GLRunnable} has been processed or queued, otherwise false
.
* @throws IllegalStateException in case the drawable is locked by this thread, no animator is running on another thread and wait
is true
.
*/
public final boolean invoke(final GLAutoDrawable drawable, boolean wait, final GLRunnable glRunnable) throws IllegalStateException {
if( null == glRunnable || null == drawable ||
wait && ( !drawable.isRealized() || null==drawable.getContext() ) ) {
return false;
}
GLRunnableTask rTask = null;
final Object rTaskLock = new Object();
Throwable throwable = null;
synchronized(rTaskLock) {
boolean deferredHere;
synchronized(glRunnablesLock) {
final boolean isGLThread = drawable.isThreadGLCapable();
deferredHere = isAnimatorAnimatingOnOtherThread();
if( deferredHere ) {
if( wait && isLockedByThisThread(drawable) ) {
if( isGLThread ) {
// Run immediately, don't defer since locked by this thread, but isGLThread
deferredHere = false;
} else {
// Locked by this thread, but _not_ isGLThread -> ERROR
throw new IllegalStateException("Deferred, wait, isLocked on current and not GL-Thread: thread "+Thread.currentThread());
}
}
} else {
if( !isGLThread && isLockedByThisThread(drawable) ) {
// Will be deferred on GL thread by display() (blocking), but locked by this thread -> ERROR
throw new IllegalStateException("Not deferred, isLocked on current and not GL-Thread: thread "+Thread.currentThread());
}
wait = false; // don't wait if exec immediately
}
rTask = new GLRunnableTask(glRunnable,
wait ? rTaskLock : null,
wait /* catch Exceptions if waiting for result */);
glRunnables.add(rTask);
}
if( !deferredHere ) {
drawable.display();
} else if( wait ) {
try {
rTaskLock.wait(); // free lock, allow execution of rTask
} catch (final InterruptedException ie) {
throwable = ie;
}
if(null==throwable) {
throwable = rTask.getThrowable();
}
if(null!=throwable) {
throw new RuntimeException(throwable);
}
}
}
return true;
}
/**
* @see #invoke(GLAutoDrawable, boolean, GLRunnable)
*
* @param drawable
* @param wait
* @param newGLRunnables
* @return
* @throws IllegalStateException
*/
public final boolean invoke(final GLAutoDrawable drawable, boolean wait, final ListsetExclusiveContextThread(null)
has been called.
*
* Default non-exclusive behavior is requested via setExclusiveContextThread(null)
,
* 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 null
.
*
* To release a previous made exclusive thread, a user issues setExclusiveContextThread(null)
* and may poll {@link #getExclusiveContextThread()} until it returns null
,
* while the exclusive thread is still running.
*
* Note: Setting a new exclusive thread without properly releasing a previous one * will throw an GLException. *
** One scenario could be to dedicate the context to the {@link com.jogamp.opengl.util.AnimatorBase#getThread() animator thread} * and spare redundant context switches. *
* @param t the exclusive thread to claim the context, ornull
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 Thread setExclusiveContextThread(final Thread t, final 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 && context.isCurrent() ) {
try {
forceNativeRelease(context);
} catch (final Throwable ex) {
flushGLRunnables();
throw GLException.newGLException(ex);
}
}
exclusiveContextThread = t;
}
if (DEBUG) {
System.err.println("GLDrawableHelper.setExclusiveContextThread(): END switch "+getExclusiveContextSwitchString()+", thread "+exclusiveContextThread+" -- currentThread "+Thread.currentThread());
}
return oldExclusiveContextThread;
}
/**
* @see #setExclusiveContextThread(Thread, GLContext)
*/
public final Thread getExclusiveContextThread() {
return exclusiveContextThread;
}
private static final ThreadLocalNote: Locking of the surface is implicit done by {@link GLContext#makeCurrent()}, where unlocking is performed by the latter {@link GLContext#release()}.
* * @param drawable * @param context * @param runnable * @param initAction */ public final void invokeGL(final GLDrawable drawable, final GLContext context, final Runnable runnable, final Runnable initAction) { if(null==context) { if (DEBUG) { GLException.dumpThrowable("informal", new GLException("Info: GLDrawableHelper " + this + ".invokeGL(): NULL GLContext")); } return; } if(PERF_STATS) { invokeGLImplStats(drawable, context, runnable, initAction); } else { invokeGLImpl(drawable, context, runnable, initAction); } } /** * Principal helper method which runs * {@link #disposeAllGLEventListener(GLAutoDrawable, boolean) disposeAllGLEventListener(autoDrawable, false)} * with the context made current. *
* If destroyContext
is true
the context is destroyed in the end while holding the lock.
*
* If destroyContext
is false
the context is natively released, i.e. released as often as locked before.
*