Bug 316

Summary: Multi-Head issues, identical to issue 241
Product: [JogAmp] Jogl Reporter: Sven Gothel <sgothel>
Component: coreAssignee: Sven Gothel <sgothel>
Status: VERIFIED FIXED    
Severity: normal    
Priority: P1    
Version: 1   
Hardware: All   
OS: linux   
Type: DEFECT SCM Refs:
Workaround: ---
Attachments: Demo that illustrates TwinView error
Proposed patch to GLCanvas to choose a visual as late as possible, seems to fix checkGD issues on Linux/Xorg/Xinerama.

Description Sven Gothel 2010-03-24 07:50:49 CET


---- Reported by moorej 2007-09-07 10:30:30 ----

Basically, I am creating a JFrame and adding a GLCanvas to it.  This sometimes
fails due to java.awt.Component.checkGD() throwing an IllegalArgumentException.
 This appears to be due to the fact that the GLCanvas and the JFrame have
different GraphicsDevice instances associated with them.  Component.checkGD()
simply checks the id string associated with the device for equality:
 
   void checkGD(String stringID) {
        if (graphicsConfig != null) {
            if (!graphicsConfig.getDevice().getIDstring().equals(stringID)) {
                throw new IllegalArgumentException(
                                                   "adding a container to a
container on a different GraphicsDevice");
            }
        }
    }
 
On my system (back when I had two heads...) I was using NVidia's driver
(currently 100.14.11) in TwinView mode with Xorg (7.0, 7.1) on Linux 2.6.22-x64,
I see two GraphicsDevices, one corresponding to each head.  These have ID
strings something like ':0.0' and ':0.1' respectively.
 
It may be possible to work around the problem by detecting the graphics device
used by the container I am adding the GLCanvas to, and setting the GC on the
GLCanvas (during construction?), however, since the GLCanvas seems to work fine
when moving the canvas (and parent container) across heads, I suspect that it
may be possible to solve this within JOGL...
 
The attached code can be compiled (with jogl/gluegen-rt in the classpath) and
run with no arguments.  It will attempt to identify a pair of GraphicsDevices
that have identical id strings, except for the last part.  If successful, it
will open a JFrame on one of them, and attempt to add a GLCanvas.  If no
exception is thrown, it will move the JFrame to the other head and try to add
another GLCanvas.  This reliably demonstrates the issue on my system.


FILE : JOGLWithNVidiaTwinView.java

import java.awt.EventQueue;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ContainerAdapter;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

import javax.media.opengl.GLCanvas;
import javax.swing.JFrame;

/**
 * Class to exhibit issue with JOGL on Linux/Xorg/NVidia/TwinView.
 * 
 * This will cause an IllegalArgumentException to be thrown when adding a
GLCanvas to
 * a JFrame (the JFrame's content pane):
 * 
 * java.lang.IllegalArgumentException: adding a container to a container on a
different GraphicsDevice
    at java.awt.Component.checkGD(Component.java:965)
    at java.awt.Container.addImpl(Container.java:1027)
    at java.awt.Container.add(Container.java:352)
    at JOGLWithNVidiaTwinView$2.run(JOGLWithNVidiaTwinView.java:88)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
    at
java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273)
    at
java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183)
    at
java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:121)
 */
public class JOGLWithNVidiaTwinView {

    /**
     * @param args Not used
     */
    public static void main(String[] args) {
        final XScreenDevice[] twinViewPair = XScreenDevice.findTwinViewScreens();
        if (twinViewPair == null) {
            System.out.println("No TwinView screen pair was found, exiting.");
            System.exit(1);
        }

        /*
         * Create a new Window on one head.
         */
        final JFrame frame = new
JFrame(twinViewPair[0].device.getDefaultConfiguration());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(200, 200);
        /*
         * Set the frame visible. We will wait until the window is opened before
         * moving it.
         */
        try {
            openAndWait(frame);

            System.out.println("Frame is on device with ID: "
                            +
frame.getGraphicsConfiguration().getDevice().getIDstring());
            System.out.flush();
            addCanvas(frame);

            System.out.println("Moving window to second device...");
            System.out.flush();
            /*
             * Get the bounds of the second device and move the frame there.
             */
            final Rectangle secondDeviceBounds =
twinViewPair[1].device.getDefaultConfiguration().getBounds();

            moveTo(frame, secondDeviceBounds);

            System.out.println("Frame is on device with ID: "
                            +
frame.getGraphicsConfiguration().getDevice().getIDstring());
            System.out.println("Adding GLCanvas...");
            System.out.flush();
            addCanvas(frame);
        } catch (InterruptedException ie) {
            System.out.println("Interrupted...");
        }
    }

    /**
     * Creates and adds a new GLCanvas to the specified JFrame, blocking
     * until the operation is complete (do not call this from the Event Thread)
     * 
     * @param frame
     * @throws InterruptedException
     */
    static void addCanvas(final JFrame frame) throws InterruptedException {
        final boolean[] added = new boolean[] { false };
        final GLCanvas canvas = new GLCanvas();
        final ContainerListener listener = new ContainerAdapter() {

            @Override
            public void componentAdded(ContainerEvent e) {
                if (e.getComponent() == canvas) {
                    synchronized (added) {
                        added[0] = true;
                        added.notifyAll();
                    }
                }
            }

        };
        EventQueue.invokeLater(new Runnable() {

            public void run() {
                final GLCanvas canvas = new GLCanvas();
                try {
                    frame.getContentPane().add(canvas);
                } catch (IllegalArgumentException iae) {
                    System.out.println("Caught exception while adding canvas: ");
                    System.out.flush();
                    iae.printStackTrace();
                }
            }
        });
        try {
            synchronized (added) {
                while (!added[0])
                    added.wait();
            }
        } finally {
            frame.removeContainerListener(listener);
        }
    }

    /**
     * Moves the specified frame somewhere (top-left corner) of the specified
rectangle, blocking until
     * the operation is complete (do not call this from the Event Thread).
     * 
     * @param frame
     * @param target
     * @throws InterruptedException
     */
    static void moveTo(final JFrame frame, final Rectangle target) throws
InterruptedException {
        final boolean[] moved = new boolean[] { target.contains(frame.getX(),
frame.getY(), frame.getWidth(), frame
                        .getHeight()) };
        final ComponentListener listener = new ComponentAdapter() {

            public void componentMoved(ComponentEvent e) {
                synchronized (moved) {
                    if (moved[0] = target.contains(frame.getX(), frame.getY(),
frame.getWidth(), frame.getHeight())) {
                        moved.notifyAll();
                        System.out.println("Moved to: " + frame.getX() + ", " +
frame.getY());
                    } else {
                        System.out.println("Moved somewhere unexpected: " +
frame.getX() + ", " + frame.getY());
                    }
                }
            }
        };
        frame.addComponentListener(listener);
        EventQueue.invokeLater(new Runnable() {

            public void run() {
                frame.setLocation(target.x, target.y);
            }
        });
        try {
            synchronized (moved) {
                while (!moved[0])
                    moved.wait();
            }
        } finally {
            frame.removeComponentListener(listener);
        }
    }

    /**
     * Sets the specified frame visible and waits for a corresponding WindowEvent.  
     * Do not call from Event Thread.
     * 
     * @param frame
     * @throws InterruptedException
     */
    static void openAndWait(JFrame frame) throws InterruptedException {
        final boolean[] open = new boolean[] { frame.isVisible() };
        final WindowListener listener = new WindowAdapter() {

            public void windowOpened(WindowEvent e) {
                synchronized (open) {
                    open[0] = true;
                    open.notifyAll();
                }
            }
        };
        frame.addWindowListener(listener);
        frame.setVisible(true);
        try {
            synchronized (open) {
                while (!open[0])
                    open.wait();
            }
        } finally {
            frame.removeWindowListener(listener);
        }
    }

    /**
     * Class to extract some (Unix, or possible Linux-specific)
     * info from a GraphicsDevice instance's idString.
     */
    static class XScreenDevice {

        public final GraphicsDevice device;
        public final String hostName;
        public final String serverID;
        public final String displayID;

        public XScreenDevice(GraphicsDevice device) {
            this.device = device;
            String idStr = device.getIDstring();

            final int colonIdx = idStr.indexOf(':');
            if (colonIdx < 0) {
                throw new IllegalArgumentException("The specified device (id: "
+ idStr
                                + ") does not appear to be an X screen device.");
            }
            hostName = idStr.substring(0, colonIdx);

            idStr = idStr.substring(colonIdx + 1);
            final int dotIdx = idStr.indexOf('.');
            if (dotIdx < 0) {
                displayID = "";
                serverID = idStr;
            } else {
                serverID = idStr.substring(0, dotIdx);
                displayID = idStr.substring(dotIdx + 1);
            }
        }

        /**
         * Checks whether the specified device might be involved in a TwinView
         * configuration with this device. This means that the hostNames and
         * serverIDs must be identical, and the displayID and device must be
         * different.
         * 
         * @param device A non-null XScreenDevice.
         * @return <code>true</code> if the specified device is (probably) in
         *         a TwinView configuration with this device, false otherwise.
         */
        public boolean isTwinViewComplement(XScreenDevice device) {
            return device.device != this.device &&
device.hostName.equals(this.hostName)
                            && device.hostName.equals(this.hostName) &&
!device.displayID.equals(this.displayID);
        }

        public String toString() {
            return "X Screen Device -- id: " + device.getIDstring() + " (host="
+ hostName + ", serverID=" + serverID
                            + ", displayID=" + displayID + ")";
        }

        /**
         * Identifies a pair of TwinView screens in the local graphics
         * environment. A TwinView pair (for the purposes of this method) has X
         * display IDs comprised of identical hostnames, followed by a colon,
         * identical server ids, followed by a '.', and unique display ids. For
         * example, ':0.0' and ':0.1' would be a pair.
         * 
         * 
         * @return A pair of XScreenDevice instances representing two distinct
         *         GraphicsDevices in the local environment that are believed to
         *         be a TwinView pair, or null if none are found.
         */
        public static XScreenDevice[] findTwinViewScreens() {
            final GraphicsEnvironment graphicsEnv =
GraphicsEnvironment.getLocalGraphicsEnvironment();
            final GraphicsDevice[] screenDevices = graphicsEnv.getScreenDevices();
            if (screenDevices == null || screenDevices.length < 2) {
                /*
                 * Not enough devices for a TwinView pair.
                 */
                return null;
            }
            for (int firstScreenIdx = 1; firstScreenIdx < screenDevices.length;
firstScreenIdx++) {
                final XScreenDevice firstDevice;
                try {
                    firstDevice = new XScreenDevice(screenDevices[firstScreenIdx]);
                    System.out.println("Searching for TwinView complement to: "
+ firstDevice);
                } catch (IllegalArgumentException iae) {
                    System.out.println("Device: " +
screenDevices[firstScreenIdx].getIDstring()
                                    + " does not appear to be an X screen");
                    continue;
                }

                for (int secondScreenIdx = firstScreenIdx - 1; secondScreenIdx
>= 0; secondScreenIdx--) {
                    try {
                        final XScreenDevice secondDevice = new
XScreenDevice(screenDevices[secondScreenIdx]);
                        System.out.println("\tTesting: " + secondDevice);
                        if (firstDevice.isTwinViewComplement(secondDevice)) {
                            System.out.println("TwinView Pair Identified!");
                            return new XScreenDevice[] { firstDevice,
secondDevice };
                        }
                    } catch (IllegalArgumentException iae) {
                        /*
                         * Already warned above...
                         */
                    }
                }
            }
            /*
             * None found.
             */
            return null;
        }
    }
}



---- Additional Comments From moorej 2007-09-07 10:31:37 ----

Created an attachment
Demo that illustrates TwinView error




---- Additional Comments From krisher 2007-09-28 09:28:18 ----

Created an attachment
Proposed patch to GLCanvas to choose a visual as late as possible, seems to fix checkGD issues on Linux/Xorg/Xinerama.




---- Additional Comments From kbr 2007-10-18 23:13:10 ----

Thanks for the patch and sorry for not looking into it until now. This fix is
excellent, basically the best that can be done under the circumstances. Fix has
been checked in and will be present in tomorrow's 1.1.1-rc6 build.




---- Additional Comments From kbr 2007-10-18 23:14:24 ----

*** Issue 241 has been marked as a duplicate of this issue. ***



--- Bug imported by sgothel@jausoft.com 2010-03-24 07:50 EDT  ---

This bug was previously known as _bug_ 316 at https://jogl.dev.java.net/bugs/show_bug.cgi?id=316
Imported an attachment (id=106)
Imported an attachment (id=107)

Unknown bug field "has_duplicates" encountered while moving bug
   <has_duplicates>
     <bug_id>241</bug_id>
     <who>kbr</who>
     <when>2007-10-18 23:14:37</when>
</has_duplicates>
The original submitter of attachment 106 [details] is unknown.
   Reassigning to the person who moved it here: sgothel@jausoft.com.
The original submitter of attachment 107 [details] is unknown.
   Reassigning to the person who moved it here: sgothel@jausoft.com.

Comment 1 Sven Gothel 2010-03-24 07:53:39 CET
*** Bug 241 has been marked as a duplicate of this bug. ***