/* * Copyright (c) 2003 Sun Microsystems, Inc. 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 * MIDROSYSTEMS, 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 net.java.games.jogl; import java.awt.Component; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.security.*; import javax.swing.JComponent; import javax.swing.JPanel; import net.java.games.jogl.impl.*; // FIXME: Subclasses need to call resetGLFunctionAvailability() on their // context whenever the displayChanged() function is called on their // GLEventListeners /** A lightweight Swing component which provides OpenGL rendering support. Provided for compatibility with Swing user interfaces when adding a heavyweight doesn't work either because of Z-ordering or LayoutManager problems. This component attempts to use hardware-accelerated rendering via pbuffers and falls back on to software rendering if problems occur. This class can not be instantiated directly; use {@link GLDrawableFactory} to construct them. */ public final class GLJPanel extends JPanel implements GLDrawable { protected static final boolean DEBUG = Debug.debug("GLJPanel"); private GLDrawableHelper drawableHelper = new GLDrawableHelper(); private volatile boolean isInitialized; // Data used for either pbuffers or pixmap-based offscreen surfaces private GLCapabilities offscreenCaps; private GLCapabilitiesChooser chooser; private GLDrawable shareWith; private BufferedImage offscreenImage; private int neededOffscreenImageWidth; private int neededOffscreenImageHeight; private DataBufferByte dbByte; private DataBufferInt dbInt; private int panelWidth = 0; private int panelHeight = 0; private Updater updater; private int awtFormat; private int glFormat; private int glType; // Implementation using pbuffers private static boolean hardwareAccelerationDisabled = Debug.isPropertyDefined("jogl.gljpanel.nohw"); private boolean pbufferInitializationCompleted; private GLPbuffer pbuffer; private int pbufferWidth = 256; private int pbufferHeight = 256; private GLCanvas heavyweight; private Frame toplevel; // Implementation using software rendering private GLContext offscreenContext; // For saving/restoring of OpenGL state during ReadPixels private int[] swapbytes = new int[1]; private int[] rowlength = new int[1]; private int[] skiprows = new int[1]; private int[] skippixels = new int[1]; private int[] alignment = new int[1]; GLJPanel(GLCapabilities capabilities, GLCapabilitiesChooser chooser, GLDrawable shareWith) { super(); // Works around problems on many vendors' cards; we don't need a // back buffer for the offscreen surface anyway offscreenCaps = (GLCapabilities) capabilities.clone(); offscreenCaps.setDoubleBuffered(false); this.chooser = chooser; this.shareWith = shareWith; } public void display() { if (!isInitialized) { return; } if (EventQueue.isDispatchThread()) { // Want display() to be synchronous, so call paintImmediately() paintImmediately(0, 0, getWidth(), getHeight()); } else { // Multithreaded redrawing of Swing components is not allowed, // so do everything on the event dispatch thread try { EventQueue.invokeAndWait(paintImmediatelyAction); } catch (Exception e) { throw new GLException(e); } } } /** Overridden from JComponent; calls {@link GLEventListener#display}. Should not be invoked by applications directly. */ public void paintComponent(Graphics g) { if (!isInitialized) { return; } updater.setGraphics(g); if (!hardwareAccelerationDisabled) { if (!pbufferInitializationCompleted) { try { heavyweight.display(); pbuffer.display(); } catch (GLException e) { // We consider any exception thrown during updating of the // heavyweight or pbuffer during the initialization phases // to be an indication that there was a problem // instantiating the pbuffer, regardless of whether the // exception originated in the user's GLEventListener. In // these cases we immediately back off and use software // rendering. disableHardwareRendering(); } } else { pbuffer.display(); } } else { offscreenContext.invokeGL(displayAction, false, initAction); } } public void addNotify() { super.addNotify(); initialize(); if (DEBUG) { System.err.println("GLJPanel.addNotify()"); } } /** Overridden from JPanel; used to indicate that it's no longer safe to have an OpenGL context for the component. */ public void removeNotify() { if (DEBUG) { System.err.println("GLJPanel.removeNotify()"); } if (!hardwareAccelerationDisabled) { if (pbuffer != null) { pbuffer.destroy(); } if (toplevel != null) { toplevel.dispose(); } pbuffer = null; heavyweight = null; toplevel = null; } else { offscreenContext.destroy(); } isInitialized = false; super.removeNotify(); } /** Overridden from Canvas; causes {@link GLDrawableHelper#reshape} to be called on all registered {@link GLEventListener}s. Called automatically by the AWT; should not be invoked by applications directly. */ public void reshape(int x, int y, int width, int height) { super.reshape(x, y, width, height); if (!isInitialized) { return; } // Move all reshape requests onto AWT EventQueue thread final int fx = x; final int fy = y; final int fwidth = width; final int fheight = height; Runnable r = new Runnable() { public void run() { GLContext context = null; neededOffscreenImageWidth = 0; neededOffscreenImageHeight = 0; if (!hardwareAccelerationDisabled) { if (fwidth > pbufferWidth || fheight > pbufferHeight) { // Must destroy and recreate pbuffer to fit pbuffer.destroy(); if (fwidth > pbufferWidth) { pbufferWidth = getNextPowerOf2(fwidth); } if (fheight > pbufferHeight) { pbufferHeight = getNextPowerOf2(fheight); } initialize(); } GLPbufferImpl pbufferImpl = (GLPbufferImpl) pbuffer; context = pbufferImpl.getContext(); // It looks like NVidia's drivers (at least the ones on my // notebook) are buggy and don't allow a rectangle of less than // the pbuffer's width to be read...this doesn't really matter // because it's the Graphics.drawImage() calls that are the // bottleneck. Should probably make the size of the offscreen // image be the exact size of the pbuffer to save some work on // resize operations... neededOffscreenImageWidth = pbufferWidth; neededOffscreenImageHeight = fheight; } else { offscreenContext.resizeOffscreenContext(fwidth, fheight); context = offscreenContext; neededOffscreenImageWidth = fwidth; neededOffscreenImageHeight = fheight; } if (offscreenImage != null && (offscreenImage.getWidth() != neededOffscreenImageWidth || offscreenImage.getHeight() != neededOffscreenImageHeight)) { offscreenImage.flush(); offscreenImage = null; } panelWidth = fwidth; panelHeight = fheight; context.invokeGL(new Runnable() { public void run() { getGL().glViewport(0, 0, panelWidth, panelHeight); drawableHelper.reshape(GLJPanel.this, 0, 0, panelWidth, panelHeight); } }, true, initAction); } }; if (EventQueue.isDispatchThread()) { r.run(); } else { // Avoid blocking EventQueue thread due to possible deadlocks // during component creation EventQueue.invokeLater(r); } } public void addGLEventListener(GLEventListener listener) { drawableHelper.addGLEventListener(listener); } public void removeGLEventListener(GLEventListener listener) { drawableHelper.removeGLEventListener(listener); } public GL getGL() { if (!hardwareAccelerationDisabled) { if (pbuffer == null) { return null; } return pbuffer.getGL(); } else { if (offscreenContext == null) { return null; } return offscreenContext.getGL(); } } public void setGL(GL gl) { if (!hardwareAccelerationDisabled) { if (pbuffer != null) { pbuffer.setGL(gl); } } else { if (offscreenContext != null) { offscreenContext.setGL(gl); } } } public GLU getGLU() { if (!hardwareAccelerationDisabled) { return pbuffer.getGLU(); } else { return offscreenContext.getGLU(); } } public void setGLU(GLU glu) { if (!hardwareAccelerationDisabled) { pbuffer.setGLU(glu); } else { offscreenContext.setGLU(glu); } } public void setRenderingThread(Thread currentThreadOrNull) throws GLException { // Not supported for GLJPanel because all repaint requests must be // handled by the AWT thread } public Thread getRenderingThread() { return null; } public void setNoAutoRedrawMode(boolean noAutoRedraws) { } public boolean getNoAutoRedrawMode() { return false; } public void setAutoSwapBufferMode(boolean onOrOff) { if (!hardwareAccelerationDisabled) { pbuffer.setAutoSwapBufferMode(onOrOff); } else { offscreenContext.setAutoSwapBufferMode(onOrOff); } } public boolean getAutoSwapBufferMode() { if (!hardwareAccelerationDisabled) { return pbuffer.getAutoSwapBufferMode(); } else { return offscreenContext.getAutoSwapBufferMode(); } } public void swapBuffers() { if (!hardwareAccelerationDisabled) { pbuffer.swapBuffers(); } else { offscreenContext.invokeGL(swapBuffersAction, false, initAction); } } public boolean canCreateOffscreenDrawable() { // For now let's say no, although we could using the heavyweight // if hardware acceleration is still enabled return false; } public GLPbuffer createOffscreenDrawable(GLCapabilities capabilities, int initialWidth, int initialHeight) { throw new GLException("Not supported"); } GLContext getContext() { if (!hardwareAccelerationDisabled) { return ((GLPbufferImpl) pbuffer).getContext(); } else { return offscreenContext; } } //---------------------------------------------------------------------- // Internals only below this point // private void disableHardwareRendering() { if (Debug.verbose()) { System.err.println("GLJPanel: Falling back on software rendering due to pbuffer problems"); } hardwareAccelerationDisabled = true; pbufferInitializationCompleted = false; EventQueue.invokeLater(new Runnable() { public void run() { toplevel.setVisible(false); // Should dispose of this -- not sure about stability on // various cards -- should test (FIXME) // toplevel.dispose(); } }); initialize(); } private void initialize() { // Initialize either the hardware-accelerated rendering path or // the lightweight rendering path if (!hardwareAccelerationDisabled) { boolean firstTime = false; if (heavyweight == null) { // Make the heavyweight share with the "shareWith" parameter. // The pbuffer shares textures and display lists with the // heavyweight, so by transitivity the pbuffer will share with // it as well. heavyweight = GLDrawableFactory.getFactory().createGLCanvas(new GLCapabilities(), shareWith); firstTime = true; } if (heavyweight.canCreateOffscreenDrawable()) { if (firstTime) { toplevel = new Frame(); toplevel.setUndecorated(true); } pbuffer = heavyweight.createOffscreenDrawable(offscreenCaps, pbufferWidth, pbufferHeight); updater = new Updater(); pbuffer.addGLEventListener(updater); pbufferInitializationCompleted = false; if (firstTime) { toplevel.add(heavyweight); toplevel.setSize(0, 0); } EventQueue.invokeLater(new Runnable() { public void run() { try { toplevel.setVisible(true); } catch (GLException e) { disableHardwareRendering(); } } }); isInitialized = true; return; } else { // If the heavyweight reports that it can't create an // offscreen drawable (pbuffer), don't try again the next // time, and fall through to the software rendering path hardwareAccelerationDisabled = true; } } // Create an offscreen context instead offscreenContext = GLContextFactory.getFactory().createGLContext(null, offscreenCaps, chooser, GLContextHelper.getContext(shareWith)); offscreenContext.resizeOffscreenContext(panelWidth, panelHeight); updater = new Updater(); if (panelWidth > 0 && panelHeight > 0) { offscreenContext.invokeGL(new Runnable() { public void run() { getGL().glViewport(0, 0, panelWidth, panelHeight); drawableHelper.reshape(GLJPanel.this, 0, 0, panelWidth, panelHeight); } }, true, initAction); } isInitialized = true; } class Updater implements GLEventListener { private Graphics g; public void setGraphics(Graphics g) { this.g = g; } public void init(GLDrawable drawable) { if (!hardwareAccelerationDisabled) { if (DEBUG) { System.err.println("GLJPanel$Updater.init(): pbufferInitializationCompleted = true"); } pbufferInitializationCompleted = true; EventQueue.invokeLater(new Runnable() { public void run() { // Race conditions might dispose of this before now if (toplevel != null) { toplevel.setVisible(false); } } }); } drawableHelper.init(GLJPanel.this); } public void display(GLDrawable drawable) { drawableHelper.display(GLJPanel.this); // Must now copy pixels from offscreen context into surface if (offscreenImage == null) { if (panelWidth > 0 && panelHeight > 0) { // It looks like NVidia's drivers (at least the ones on my // notebook) are buggy and don't allow a sub-rectangle to be // read from a pbuffer...this doesn't really matter because // it's the Graphics.drawImage() calls that are the // bottleneck int awtFormat = 0; int hwGLFormat = 0; if (!hardwareAccelerationDisabled) { // Should be more flexible in these BufferedImage formats; // perhaps see what the preferred image types are on the // given platform awtFormat = BufferedImage.TYPE_INT_RGB; // This seems to be a good choice on all platforms hwGLFormat = GL.GL_UNSIGNED_INT_8_8_8_8_REV; } else { awtFormat = offscreenContext.getOffscreenContextBufferedImageType(); } offscreenImage = new BufferedImage(neededOffscreenImageWidth, neededOffscreenImageHeight, awtFormat); switch (awtFormat) { case BufferedImage.TYPE_3BYTE_BGR: glFormat = GL.GL_BGR; glType = GL.GL_UNSIGNED_BYTE; dbByte = (DataBufferByte) offscreenImage.getRaster().getDataBuffer(); break; case BufferedImage.TYPE_INT_RGB: case BufferedImage.TYPE_INT_ARGB: glFormat = GL.GL_BGRA; glType = (hardwareAccelerationDisabled ? offscreenContext.getOffscreenContextPixelDataType() : hwGLFormat); dbInt = (DataBufferInt) offscreenImage.getRaster().getDataBuffer(); break; default: // FIXME: Support more off-screen image types (current // offscreen context implementations don't use others, and // some of the OpenGL formats aren't supported in the 1.1 // headers, which we're currently using) throw new GLException("Unsupported offscreen image type " + awtFormat); } } } if (offscreenImage != null) { GL gl = getGL(); // Save current modes gl.glGetIntegerv(GL.GL_PACK_SWAP_BYTES, swapbytes); gl.glGetIntegerv(GL.GL_PACK_ROW_LENGTH, rowlength); gl.glGetIntegerv(GL.GL_PACK_SKIP_ROWS, skiprows); gl.glGetIntegerv(GL.GL_PACK_SKIP_PIXELS, skippixels); gl.glGetIntegerv(GL.GL_PACK_ALIGNMENT, alignment); gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, GL.GL_FALSE); gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, offscreenImage.getWidth()); gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, 0); gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, 0); gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1); // Actually read the pixels. gl.glReadBuffer(GL.GL_FRONT); if (dbByte != null) { gl.glReadPixels(0, 0, offscreenImage.getWidth(), offscreenImage.getHeight(), glFormat, glType, dbByte.getData()); } else if (dbInt != null) { gl.glReadPixels(0, 0, offscreenImage.getWidth(), offscreenImage.getHeight(), glFormat, glType, dbInt.getData()); } // Restore saved modes. gl.glPixelStorei(GL.GL_PACK_SWAP_BYTES, swapbytes[0]); gl.glPixelStorei(GL.GL_PACK_ROW_LENGTH, rowlength[0]); gl.glPixelStorei(GL.GL_PACK_SKIP_ROWS, skiprows[0]); gl.glPixelStorei(GL.GL_PACK_SKIP_PIXELS, skippixels[0]); gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, alignment[0]); if (!hardwareAccelerationDisabled || offscreenContext.offscreenImageNeedsVerticalFlip()) { // This performs reasonably well; the snippet below does not. // Should figure out if we need to set the image scaling // preference to FAST since it doesn't require subsampling // of pixels -- FIXME for (int i = 0; i < panelHeight; i++) { g.drawImage(offscreenImage, 0, i, panelWidth, i+1, 0, panelHeight - i - 1, panelWidth, panelHeight - i, GLJPanel.this); } } else { g.drawImage(offscreenImage, 0, 0, offscreenImage.getWidth(), offscreenImage.getHeight(), GLJPanel.this); } } } public void reshape(GLDrawable drawable, int x, int y, int width, int height) { // This is handled above and dispatched directly to the appropriate context } public void displayChanged(GLDrawable drawable, boolean modeChanged, boolean deviceChanged) { } } class InitAction implements Runnable { public void run() { updater.init(GLJPanel.this); } } private InitAction initAction = new InitAction(); class DisplayAction implements Runnable { public void run() { updater.display(GLJPanel.this); } } private DisplayAction displayAction = new DisplayAction(); // This one is used exclusively in the non-hardware-accelerated case class SwapBuffersAction implements Runnable { public void run() { offscreenContext.swapBuffers(); } } private SwapBuffersAction swapBuffersAction = new SwapBuffersAction(); class PaintImmediatelyAction implements Runnable { public void run() { paintImmediately(0, 0, getWidth(), getHeight()); } } private PaintImmediatelyAction paintImmediatelyAction = new PaintImmediatelyAction(); private int getNextPowerOf2(int number) { if (((number-1) & number) == 0) { //ex: 8 -> 0b1000; 8-1=7 -> 0b0111; 0b1000&0b0111 == 0 return number; } int power = 0; while (number > 0) { number = number>>1; power++; } return (1<