package com.jogamp.opengl.test.junit.jogl.acore; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.swing.Timer; import org.junit.Test; import com.jogamp.common.os.Platform; import com.jogamp.common.util.InterruptSource; import com.jogamp.common.util.locks.LockFactory; import com.jogamp.common.util.locks.RecursiveLock; import com.jogamp.opengl.*; import com.jogamp.opengl.test.junit.util.DumpGLInfo; /** * Bug 1160. * Test for context sharing with an external context. Creates an external GL context, then * sets up an offscreen drawable which shares with it. The test contains two cases: one * which creates and repaints the offscreen drawable on the EDT, and one which does so on * a dedicated thread. On Windows+NVidia, the former fails. */ public class TestSharedExternalContextAWT { static final int LATCH_COUNT = 5; private void doTest(final boolean aUseEDT) throws Exception { final CountDownLatch testLatch = new CountDownLatch(1); final CountDownLatch masterLatch = new CountDownLatch(1); final CountDownLatch slaveLatch = new CountDownLatch(LATCH_COUNT); final MyGLEventListener listener = new MyGLEventListener(aUseEDT, slaveLatch); /** * For the purpose of this test, this offscreen drawable will be used to create * an external GL context. In the actual application, the external context * represents a GL context which lives outside the JVM. */ final Runnable runnable = new Runnable() { public void run() { System.err.println("Master Thread Start: "+Thread.currentThread().getName()); final GLProfile glProfile = GLProfile.getDefault(); final GLCapabilities caps = new GLCapabilities(glProfile); final GLAutoDrawable buffer = GLDrawableFactory.getDesktopFactory().createOffscreenAutoDrawable( GLProfile.getDefaultDevice(), caps, null, 512, 512 ); // The listener will set up the context sharing in its init() method. buffer.addGLEventListener(new DumpGLInfo(Platform.getNewline()+Platform.getNewline()+"Master GLContext", false, false, false)); buffer.addGLEventListener(listener); buffer.display(); masterLatch.countDown(); // wait until test has finished try { testLatch.await(); } catch (final InterruptedException e) { e.printStackTrace(); } System.err.println("Master Thread End: "+Thread.currentThread().getName()); } }; // Kick off thread creating the actual external context // which is suppose to lie outside of the JVM. // The thread is kept alive, since this detail // may be required for the OpenGL driver implementation. final Thread thread = new InterruptSource.Thread(null, runnable); thread.setDaemon(true); thread.start(); masterLatch.await(3, TimeUnit.SECONDS); // Wait for slave to finish. slaveLatch.await(3, TimeUnit.SECONDS); // signal master test has finished testLatch.countDown(); // If exceptions occurred, fail. final Exception e = listener.fException; if (e != null) { throw e; } } @Test public void test01OnEDT() throws Exception { doTest(true); } @Test public void test02OnExecutorThread() throws Exception { doTest(false); } /** * Listener that creates an external drawable and an offscreen drawable, with context * sharing between the two. */ private static class MyGLEventListener implements GLEventListener { GLOffscreenAutoDrawable fOffscreenDrawable; final boolean fUseEDT; final CountDownLatch fLatch; final RecursiveLock masterLock = LockFactory.createRecursiveLock(); private Exception fException = null; public MyGLEventListener(final boolean aUseEDT, final CountDownLatch aLatch) { fUseEDT = aUseEDT; fLatch = aLatch; } @Override public void init(final GLAutoDrawable drawable) { // FIXME: We actually need to hook into GLContext make-current lock masterLock.lock(); try { final GL2 gl = drawable.getGL().getGL2(); gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); gl.glClear(GL.GL_COLOR_BUFFER_BIT); System.err.println(); System.err.println(); System.err.println("Master (orig) Ct: "+drawable.getContext()); // Create the external context on the caller thread. final GLContext master = GLDrawableFactory.getDesktopFactory().createExternalGLContext(); System.err.println(); System.err.println(); System.err.println("External Context: "+master); // This runnable creates an offscreen drawable which shares with the external context. final Runnable initializer = new Runnable() { public void run() { // FIXME: We actually need to hook into GLContext make-current lock // masterLock.lock(); try { fOffscreenDrawable = GLDrawableFactory.getDesktopFactory().createOffscreenAutoDrawable( GLProfile.getDefaultDevice(), new GLCapabilities(GLProfile.getDefault()), null, // new DefaultGLCapabilitiesChooser(), 512, 512 ); fOffscreenDrawable.setSharedContext(master); fOffscreenDrawable.addGLEventListener(new DumpGLInfo(Platform.getNewline()+Platform.getNewline()+"Slave GLContext", false, false, false)); try { System.err.println(); System.err.println(); System.err.println("Current: "+GLContext.getCurrent()); fOffscreenDrawable.display(); } catch (final GLException e) { fException = e; throw e; } } finally { // masterLock.unlock(); } } }; /** * Depending on the test case, invoke the initialization on the EDT or on an * executor thread. The test also displays the offscreen drawable a few times * before finishing. */ if (fUseEDT) { // Initialize using invokeLater(). try { // We cannot use EventQueue.invokeAndWait(..) since it will // block this will block the current thread, holding the context! // The whole issue w/ an external shared context is make-current // synchronization. JOGL attempts to lock the surface/drawable // of the master context to avoid concurrent usage. // The semantic constraints of a shared context are not well defined, // i.e. some driver may allow creating a shared context w/ a master context // to be in use - others don't. // Hence it is up to the user to sync the external master context in this case, // see 'masterLock' of in this code! // EventQueue.invokeAndWait(initializer); EventQueue.invokeLater(initializer); } catch (final Exception e) { fException = e; } // Display using a Swing timer, i.e. also on the EDT. final Timer t = new Timer(200, new ActionListener() { int i = 0; @Override public void actionPerformed(final ActionEvent e) { if (++i > LATCH_COUNT) { return; } System.err.println("Update on EDT"); fOffscreenDrawable.display(); fLatch.countDown(); } }); t.start(); } else { // Initialize and display using a single-threaded executor. final ScheduledExecutorService exe = Executors.newSingleThreadScheduledExecutor(); exe.submit(initializer); exe.scheduleAtFixedRate(new Runnable() { int i = 0; @Override public void run() { if (++i > LATCH_COUNT) { return; } System.err.println("Update on Executor thread"); fOffscreenDrawable.display(); fLatch.countDown(); } }, 0, 200, TimeUnit.MILLISECONDS); } } finally { masterLock.unlock(); } } @Override public void dispose(final GLAutoDrawable drawable) { // FIXME: We actually need to hook into GLContext make-current lock masterLock.lock(); try { } finally { masterLock.unlock(); } } @Override public void display(final GLAutoDrawable drawable) { // FIXME: We actually need to hook into GLContext make-current lock masterLock.lock(); try { } finally { masterLock.unlock(); } } @Override public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { // FIXME: We actually need to hook into GLContext make-current lock masterLock.lock(); try { } finally { masterLock.unlock(); } } } public static void main(final String[] pArgs) { org.junit.runner.JUnitCore.main(TestSharedExternalContextAWT.class.getName()); } }