/** * Copyright 2012 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.test.junit.jogl.awt; import java.applet.Applet; import java.awt.BorderLayout; import java.awt.Color; import java.awt.DisplayMode; import java.awt.EventQueue; import java.awt.Frame; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.InvocationTargetException; import java.util.Timer; import java.util.TimerTask; import com.jogamp.opengl.*; import com.jogamp.opengl.awt.GLCanvas; import org.junit.Assume; import org.junit.Test; import org.junit.FixMethodOrder; import org.junit.runners.MethodSorters; import com.jogamp.common.os.Platform; import com.jogamp.common.util.VersionNumber; import com.jogamp.common.util.awt.AWTEDTExecutor; import com.jogamp.opengl.util.AnimatorBase; import com.jogamp.opengl.test.junit.util.MiscUtils; import com.jogamp.opengl.test.junit.util.UITestCase; /** * Sample program that relies on JOGL's mechanism to handle the OpenGL context * and rendering loop when using an AWT canvas attached to an Applet. * <p> * BUG on OSX/CALayer w/ Java6: * If frame.setTitle() is issued right after initialization the call hangs in * <pre> * at apple.awt.CWindow._setTitle(Native Method) * at apple.awt.CWindow.setTitle(CWindow.java:765) [1.6.0_37, build 1.6.0_37-b06-434-11M3909] * </pre> * </p> * <p> * OSX/CALayer is forced by using an Applet component in this unit test. * </p> * <p> * Similar deadlock has been experienced w/ other mutable operation on an AWT Container owning a GLCanvas child, * e.g. setResizable*(). * </p> * <p> * Users shall make sure all mutable AWT calls are performed on the EDT, even before 1st setVisible(true) ! * </p> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestGLCanvasAWTActionDeadlock02AWT extends UITestCase { static int framesPerTest = 240; // frames static class MiniPApplet extends Applet implements MouseMotionListener, KeyListener { private static final long serialVersionUID = 1L; ///////////////////////////////////////////////////////////// // // Test parameters public int frameRate = 120; public int numSamples = 4; public boolean fullScreen = false; public boolean useAnimator = true; public boolean resizeableFrame = true; public boolean restartCanvas = true; public int restartTimeout = 100; // in number of frames. public boolean printThreadInfo = false; public boolean printEventInfo = false; ///////////////////////////////////////////////////////////// // // Internal variables int width; int height; String OPENGL_VENDOR; String OPENGL_RENDERER; String OPENGL_VERSION; String OPENGL_EXTENSIONS; int currentSamples = -1; private Frame frame; private GLProfile profile; private GLCapabilities capabilities; private GLCanvas canvas; private SimpleListener listener; private CustomAnimator animator; private long beforeTime; private long overSleepTime; private final long frameRatePeriod = 1000000000L / frameRate; private boolean initialized = false; private boolean osxCALayerAWTModBug = false; boolean justInitialized = true; private double theta = 0; private double s = 0; private double c = 0; private long millisOffset; private int fcount, lastm; private float frate; private final int fint = 3; private boolean setFramerate = false; private boolean restarted = false; private int frameCount = 0; void run() throws InterruptedException, InvocationTargetException { // Thread loop = new Thread("Animation Thread") { // public void run() { frameCount = 0; while ( frameCount < framesPerTest ) { if (!initialized) { setup(); } if (restartCanvas && restartTimeout == frameCount) { restart(); } if (useAnimator) { animator.requestRender(); } else { canvas.display(); } clock(); frameCount++; if( null == frame ) { break; } } dispose(); // } // }; // loop.start(); } void setup() throws InterruptedException, InvocationTargetException { if (printThreadInfo) System.out.println("Current thread at setup(): " + Thread.currentThread()); millisOffset = System.currentTimeMillis(); final VersionNumber version170 = new VersionNumber(1, 7, 0); osxCALayerAWTModBug = Platform.OSType.MACOS == Platform.getOSType() && 0 > Platform.getJavaVersionNumber().compareTo(version170); System.err.println("OSX CALayer AWT-Mod Bug "+osxCALayerAWTModBug); System.err.println("OSType "+Platform.getOSType()); System.err.println("Java Version "+Platform.getJavaVersionNumber()); // Frame setup ---------------------------------------------------------- width = 300; height = 300; final MiniPApplet applet = this; final GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); final GraphicsDevice displayDevice = environment.getDefaultScreenDevice(); frame = new Frame(displayDevice.getDefaultConfiguration()); final Rectangle fullScreenRect; if (fullScreen) { final DisplayMode mode = displayDevice.getDisplayMode(); fullScreenRect = new Rectangle(0, 0, mode.getWidth(), mode.getHeight()); } else { fullScreenRect = null; } // All AWT Mods on AWT-EDT, especially due to the follow-up complicated code! AWTEDTExecutor.singleton.invoke(true, new Runnable() { public void run() { frame.setTitle("MiniPApplet"); } } ); if (fullScreen) { try { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { frame.setUndecorated(true); frame.setBackground(Color.GRAY); frame.setBounds(fullScreenRect); frame.setVisible(true); }}); } catch (final Throwable t) { t.printStackTrace(); Assume.assumeNoException(t); } } try { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { frame.setLayout(null); frame.add(applet); if (fullScreen) { frame.invalidate(); } else { frame.pack(); } frame.setResizable(resizeableFrame); if (fullScreen) { // After the pack(), the screen bounds are gonna be 0s frame.setBounds(fullScreenRect); applet.setBounds((fullScreenRect.width - applet.width) / 2, (fullScreenRect.height - applet.height) / 2, applet.width, applet.height); } else { final Insets insets = frame.getInsets(); final int windowW = applet.width + insets.left + insets.right; final int windowH = applet.height + insets.top + insets.bottom; final int locationX = 100; final int locationY = 100; frame.setSize(windowW, windowH); frame.setLocation(locationX, locationY); final int usableWindowH = windowH - insets.top - insets.bottom; applet.setBounds((windowW - width)/2, insets.top + (usableWindowH - height)/2, width, height); } }}); } catch (final Throwable t) { t.printStackTrace(); Assume.assumeNoException(t); } frame.add(this); frame.addWindowListener(new WindowAdapter() { public void windowClosing(final WindowEvent e) { try { dispose(); } catch (final Exception ex) { Assume.assumeNoException(ex); } } }); javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { frame.setVisible(true); } } ); // Canvas setup ---------------------------------------------------------- profile = GLProfile.getDefault(); capabilities = new GLCapabilities(profile); capabilities.setSampleBuffers(true); capabilities.setNumSamples(numSamples); capabilities.setDepthBits(24); // capabilities.setStencilBits(8); // No Stencil on OSX w/ hw-accel ! capabilities.setAlphaBits(8); canvas = new GLCanvas(capabilities); canvas.setBounds(0, 0, width, height); javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { MiniPApplet.this.setLayout(new BorderLayout()); MiniPApplet.this.add(canvas, BorderLayout.CENTER); MiniPApplet.this.validate(); } } ); canvas.addMouseMotionListener(this); canvas.addKeyListener(this); // Setting up animation listener = new SimpleListener(); canvas.addGLEventListener(listener); if (useAnimator) { animator = new CustomAnimator(canvas); animator.start(); } initialized = true; } void restart() throws InterruptedException, InvocationTargetException { System.out.println("Restarting surface..."); // Stopping animation, removing current canvas. if (useAnimator) { animator.stop(); animator.remove(canvas); } canvas.disposeGLEventListener(listener, true); this.remove(canvas); capabilities = new GLCapabilities(profile); capabilities.setSampleBuffers(true); capabilities.setNumSamples(numSamples); canvas = new GLCanvas(capabilities); canvas.setBounds(0, 0, width, height); // Setting up animation again javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { MiniPApplet.this.setLayout(new BorderLayout()); MiniPApplet.this.add(canvas, BorderLayout.CENTER); MiniPApplet.this.validate(); } } ); canvas.addMouseMotionListener(this); canvas.addKeyListener(this); canvas.addGLEventListener(listener); if (useAnimator) { animator.add(canvas); animator.start(); } setFramerate = false; restarted = true; System.out.println("Done"); } void dispose() throws InterruptedException, InvocationTargetException { if( null == frame ) { return; } // Stopping animation, removing current canvas. if (useAnimator) { animator.stop(); animator.remove(canvas); } canvas.removeGLEventListener(listener); if( EventQueue.isDispatchThread() ) { MiniPApplet.this.remove(canvas); frame.remove(MiniPApplet.this); frame.validate(); frame.dispose(); frame = null; } else { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { MiniPApplet.this.remove(canvas); frame.remove(MiniPApplet.this); frame.validate(); frame.dispose(); frame = null; }}); } } void draw(final GL2 gl) { if( !osxCALayerAWTModBug || !justInitialized ) { AWTEDTExecutor.singleton.invoke(true, new Runnable() { public void run() { frame.setTitle("frame " + frameCount); } } ); } if (printThreadInfo) System.out.println("Current thread at draw(): " + Thread.currentThread()); if (OPENGL_VENDOR == null) { OPENGL_VENDOR = gl.glGetString(GL.GL_VENDOR); OPENGL_RENDERER = gl.glGetString(GL.GL_RENDERER); OPENGL_VERSION = gl.glGetString(GL.GL_VERSION); OPENGL_EXTENSIONS = gl.glGetString(GL.GL_EXTENSIONS); System.out.println(OPENGL_VENDOR); System.out.println(OPENGL_RENDERER); System.out.println(OPENGL_VERSION); System.out.println(OPENGL_EXTENSIONS); final int[] temp = { 0 }; gl.glGetIntegerv(GL2ES3.GL_MAX_SAMPLES, temp, 0); System.out.println("Maximum number of samples supported by the hardware: " + temp[0]); System.out.println("Frame: "+frame); System.out.println("Applet: "+MiniPApplet.this); System.out.println("GLCanvas: "+canvas); System.out.println("GLDrawable: "+canvas.getDelegatedDrawable()); } if (currentSamples == -1) { final int[] temp = { 0 }; gl.glGetIntegerv(GL.GL_SAMPLES, temp, 0); currentSamples = temp[0]; if (numSamples != currentSamples) { System.err.println("Requested sampling level " + numSamples + " not supported. Using " + currentSamples + " samples instead."); } } if (!setFramerate) { if (60 < frameRate) { // Disables vsync gl.setSwapInterval(0); } else if (30 < frameRate) { gl.setSwapInterval(1); } else { gl.setSwapInterval(2); } setFramerate = true; } if (restarted) { final int[] temp = { 0 }; gl.glGetIntegerv(GL.GL_SAMPLES, temp, 0); if (numSamples != temp[0]) { System.err.println("Multisampling level requested " + numSamples + " not supported. Using " + temp[0] + "samples instead."); } } gl.glClearColor(0, 0, 0, 1); gl.glClear(GL.GL_COLOR_BUFFER_BIT); theta += 0.01; s = Math.sin(theta); c = Math.cos(theta); gl.glBegin(GL.GL_TRIANGLES); gl.glColor3f(1, 0, 0); gl.glVertex2d(-c, -c); gl.glColor3f(0, 1, 0); gl.glVertex2d(0, c); gl.glColor3f(0, 0, 1); gl.glVertex2d(s, -s); gl.glEnd(); gl.glFlush(); fcount += 1; final int m = (int) (System.currentTimeMillis() - millisOffset); if (m - lastm > 1000 * fint) { frate = (float)(fcount) / fint; fcount = 0; lastm = m; System.err.println("fps: " + frate); } } void clock() { final long afterTime = System.nanoTime(); final long timeDiff = afterTime - beforeTime; final long sleepTime = (frameRatePeriod - timeDiff) - overSleepTime; if (sleepTime > 0) { // some time left in this cycle try { Thread.sleep(sleepTime / 1000000L, (int) (sleepTime % 1000000L)); } catch (final InterruptedException ex) { } overSleepTime = (System.nanoTime() - afterTime) - sleepTime; } else { // sleepTime <= 0; the frame took longer than the period overSleepTime = 0L; } beforeTime = System.nanoTime(); } class SimpleListener implements GLEventListener { @Override public void display(final GLAutoDrawable drawable) { draw(drawable.getGL().getGL2()); justInitialized = false; } @Override public void dispose(final GLAutoDrawable drawable) { } @Override public void init(final GLAutoDrawable drawable) { justInitialized = true; } @Override public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int w, final int h) { } } public void mouseDragged(final MouseEvent ev) { if (printEventInfo) { System.err.println("Mouse dragged event: " + ev); } } public void mouseMoved(final MouseEvent ev) { if (printEventInfo) { System.err.println("Mouse moved event: " + ev); } } public void keyPressed(final KeyEvent ev) { if (printEventInfo) { System.err.println("Key pressed event: " + ev); } } public void keyReleased(final KeyEvent ev) { if (printEventInfo) { System.err.println("Key released event: " + ev); } } public void keyTyped(final KeyEvent ev) { if (printEventInfo) { System.err.println("Key typed event: " + ev); } } /** An Animator subclass which renders one frame at the time * upon calls to the requestRender() method. **/ public static class CustomAnimator extends AnimatorBase { private Timer timer = null; private TimerTask task = null; private volatile boolean shouldRun; protected String getBaseName(final String prefix) { return "Custom" + prefix + "Animator" ; } /** Creates an CustomAnimator with an initial drawable to * animate. */ public CustomAnimator(final GLAutoDrawable drawable) { if (drawable != null) { add(drawable); } } public synchronized void requestRender() { shouldRun = true; } public final synchronized boolean isStarted() { return (timer != null); } public final synchronized boolean isAnimating() { return (timer != null) && (task != null); } private void startTask() { if(null != task) { return; } task = new TimerTask() { private boolean firstRun = true; public void run() { if (firstRun) { Thread.currentThread().setName("OPENGL"); firstRun = false; } if(CustomAnimator.this.shouldRun) { CustomAnimator.this.animThread = Thread.currentThread(); // display impl. uses synchronized block on the animator instance display(); synchronized (this) { // done with current frame. shouldRun = false; } } } }; fpsCounter.resetFPSCounter(); shouldRun = false; timer.schedule(task, 0, 1); } public synchronized boolean start() { if (timer != null) { return false; } timer = new Timer(); startTask(); return true; } /** Stops this CustomAnimator. */ public synchronized boolean stop() { if (timer == null) { return false; } shouldRun = false; if(null != task) { task.cancel(); task = null; } if(null != timer) { timer.cancel(); timer = null; } animThread = null; try { Thread.sleep(20); // ~ 1/60 hz wait, since we can't ctrl stopped threads / holding the lock is OK here! } catch (final InterruptedException e) { } return true; } public final synchronized boolean isPaused() { return false; } public synchronized boolean resume() { return false; } public synchronized boolean pause() { return false; } } } @Test public void test00() { TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet mini; try { final Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet.class.getName()); mini = (TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet) c.newInstance(); } catch (final Exception e) { throw new RuntimeException(e); } if (mini != null) { try { mini.run(); } catch (final Exception ex) { Assume.assumeNoException(ex); } } } public static void main(final String args[]) { for(int i=0; i<args.length; i++) { if(args[i].equals("-frames")) { framesPerTest = MiscUtils.atoi(args[++i], framesPerTest); } } org.junit.runner.JUnitCore.main(TestGLCanvasAWTActionDeadlock02AWT.class.getName()); } }