Bug 1277

Summary: GLCanvas content draws in wrong position when canvas moves after JFrame containing it is setVisible(true) on EDT
Product: [JogAmp] Jogl Reporter: Andy Edwards <jedwards>
Component: awtAssignee: Sven Gothel <sgothel>
Status: UNCONFIRMED ---    
Severity: minor CC: gouessej, robin.stevens
Priority: P4    
Version: tbd   
Hardware: pc_x86_64   
OS: macosx   
Type: DEFECT SCM Refs:
Workaround: TRUE
Attachments: Output of the test program
Test program
Expected result (screenshot from a Linux machine)
Screenshot of bug (taken on OS X)

Description Andy Edwards 2016-01-03 17:55:19 CET
JOGL version is 2.3.2, the latest available in Maven central repo.  It wasn't available for me to select.

Repro repo: https://github.com/jedwards1211/GLCanvasPositioningBug

Note: this has only been tested on Mac OS X 10.11.1, on a MacBook Pro (15-inch, Late 2011)

When the JFrame containing a GLCanvas is setVisible(true) on the main thread, the contents of the GLCanvas draw in the correct position, even if it gets moved around within the JFrame.

However, if such a JFrame is setVisible(true) on the AWT Event Dispatch Thread (e.g. inside a SwingUtilities.invokeLater call or any AWT event callback), and the GLCanvas is later moved from its initial position, the JOGL-rendered content of the GLCanvas remains aligned to its original bottom left corner position, rather than updating to the new canvas position.

As far as I understand, calling setVisible(true) on the EDT is technically the only thread-safe option, since not all AWT/Swing code is synchronized.  Calling JFrame.setVisible(true) on other threads rarely causes problems, but I think calling it on the EDT is commonly recognized as the best practice, just as all creation and interaction with AWT/Swing components should be done on the EDT.  Hence the first priority with GLCanvas should be to work when its JFrame is setVisible(true) on the EDT, and it doesn't necessarily have to work when the JFrame is setVisible(true) on another thread.
Comment 1 Robin Stevens 2016-05-03 18:19:26 CEST
Created attachment 781 [details]
Output of the test program
Comment 2 Robin Stevens 2016-05-03 18:20:27 CEST
Created attachment 782 [details]
Test program
Comment 3 Robin Stevens 2016-05-03 18:21:05 CEST
Created attachment 783 [details]
Expected result (screenshot from a Linux machine)
Comment 4 Robin Stevens 2016-05-03 18:22:08 CEST
Created attachment 784 [details]
Screenshot of bug (taken on OS X)
Comment 5 Robin Stevens 2016-05-03 18:22:53 CEST
We bumped into the same issue. I only discovered this bug after writing a small program that allows to reproduce the issue. I will attach it here.

The program is a modified version of the Gears sample (https://github.com/sgothel/jogl-demos/blob/master/src/demos/gears/Gears.java):
- The UI is created on the EDT to respect the Swing threading rules
- Animations, mouse listeners, key listeners, ... have been removed to keep the sample as simple as possible
- A button has been added above the GLCanvas. This button allows to toggle the presence of a pink panel at the bottom, underneath the GLCanvas

The expected behavior is that when the panel appears by pressing the button, the panel appears underneath the GLCanvas. This works as expected on Windows and Linux.

On OS X however, the GLCanvas remains in the same position, and is painted on top of the pink panel. In the location where the GLCanvas should have been painted, a grey area appears.

Screenshot of the result of the program on a Linux machine (correct behavior) and on an OS X machine (incorrect behavior) are attached as well.

The output of the program (including the JOGL version information) is attached. Note that we could reproduce this on all the Macs we tried (including an iMac with an AMD GPU).
Comment 6 Robin Stevens 2016-05-13 12:27:35 CEST
The specified workaround of making the GLCanvas visible on a background thread is not always an option in a larger Swing based application. Combined with the fact that deliberately violating the Swing threading rules is asking for problems.

Another workaround that respects the Swing threading rules is to remove and re-add the GLCanvas component from/to the Swing hierarchy on a resize:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import javax.swing.JPanel;
import javax.swing.Timer;

final class TLspOSXViewPositionFixer {
  /**
   * <p>
   *   On OS X, when the heavyweight component gets resized, it is painted in the wrong location.
   *   The only workaround we found is to remove and re-add the heavy weight component from/to the Swing hierarchy.
   * </p>
   *
   * <p>
   *   This method wraps that component in an extra {@code JPanel} using a {@code BorderLayout},
   *   and puts the heavy weight component in the {@code BorderLayout.CENTER}.
   *   Each time that {@code JPanel} resizes, the heavy weight component is removed from the panel and re-added.
   * </p>
   *
   * @param aComponent The heavy weight component (for example the GLCanvas)
   *
   * @return The component to add to the Swing hierarchy
   */
  static Component convertHostComponent(final Component aComponent ){
    final JPanel container = new JPanel(new BorderLayout()){

      @Override
      public void repaint(){
        super.repaint();
        aComponent.repaint();
      }
    };
    container.add(aComponent, BorderLayout.CENTER);
    container.addComponentListener(new RemoveReAddHWComponentListener(container, aHostComponent));
    return container;
  }

  private static class RemoveReAddHWComponentListener extends ComponentAdapter{
    /**
     * Delay of the timer responsible for removing and re-adding the heavy weight component
     */
    private static final int TIMER_DELAY = 150;
    /**
     * The maximum number of times the timer is restarted before it gets executed anyway
     */
    private static final int TIMER_AVOID_STARVATION = 20;

    private final JPanel container;
    private final Component component;

    private final Timer timer = new Timer(TIMER_DELAY, new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        removeAddComponent();
      }
    });

    private int numberOfTimesRestarted = 0;

    private RemoveReAddViewListener(JPanel aContainer, Component aComponent) {
      container = aContainer;
      component = aViewHostComponent;
      timer.setRepeats(false);
    }

    @Override
    public void componentResized(ComponentEvent e) {
      if( shouldRemoveAddImmediately()){
        timer.stop();
        removeAddComponent();
      }

      if ( timer.isRunning() ){
        timer.restart();
        numberOfTimesRestarted++;
      } else {
        timer.start();
      }
    }

    private boolean shouldRemoveAddImmediately(){
      return numberOfTimesRestarted > TIMER_AVOID_STARVATION;
    }

    private void removeAddComponent(){
      container.remove(component);
      container.add(component, BorderLayout.CENTER);

      numberOfTimesRestarted = 0;

      container.revalidate();
      container.repaint();
    }
  }
}
Comment 7 Julien Gouesse 2016-05-13 13:10:28 CEST
(In reply to Robin Stevens from comment #6)
> The specified workaround of making the GLCanvas visible on a background
> thread is not always an option in a larger Swing based application. Combined
> with the fact that deliberately violating the Swing threading rules is
> asking for problems.
> 
> Another workaround that respects the Swing threading rules is to remove and
> re-add the GLCanvas component from/to the Swing hierarchy on a resize:
> 
> import java.awt.BorderLayout;
> import java.awt.Component;
> import java.awt.event.ActionEvent;
> import java.awt.event.ActionListener;
> import java.awt.event.ComponentAdapter;
> import java.awt.event.ComponentEvent;
> 
> import javax.swing.JPanel;
> import javax.swing.Timer;
> 
> final class TLspOSXViewPositionFixer {
>   /**
>    * <p>
>    *   On OS X, when the heavyweight component gets resized, it is painted
> in the wrong location.
>    *   The only workaround we found is to remove and re-add the heavy weight
> component from/to the Swing hierarchy.
>    * </p>
>    *
>    * <p>
>    *   This method wraps that component in an extra {@code JPanel} using a
> {@code BorderLayout},
>    *   and puts the heavy weight component in the {@code
> BorderLayout.CENTER}.
>    *   Each time that {@code JPanel} resizes, the heavy weight component is
> removed from the panel and re-added.
>    * </p>
>    *
>    * @param aComponent The heavy weight component (for example the GLCanvas)
>    *
>    * @return The component to add to the Swing hierarchy
>    */
>   static Component convertHostComponent(final Component aComponent ){
>     final JPanel container = new JPanel(new BorderLayout()){
> 
>       @Override
>       public void repaint(){
>         super.repaint();
>         aComponent.repaint();
>       }
>     };
>     container.add(aComponent, BorderLayout.CENTER);
>     container.addComponentListener(new
> RemoveReAddHWComponentListener(container, aHostComponent));
>     return container;
>   }
> 
>   private static class RemoveReAddHWComponentListener extends
> ComponentAdapter{
>     /**
>      * Delay of the timer responsible for removing and re-adding the heavy
> weight component
>      */
>     private static final int TIMER_DELAY = 150;
>     /**
>      * The maximum number of times the timer is restarted before it gets
> executed anyway
>      */
>     private static final int TIMER_AVOID_STARVATION = 20;
> 
>     private final JPanel container;
>     private final Component component;
> 
>     private final Timer timer = new Timer(TIMER_DELAY, new ActionListener() {
>       @Override
>       public void actionPerformed(ActionEvent e) {
>         removeAddComponent();
>       }
>     });
> 
>     private int numberOfTimesRestarted = 0;
> 
>     private RemoveReAddViewListener(JPanel aContainer, Component aComponent)
> {
>       container = aContainer;
>       component = aViewHostComponent;
>       timer.setRepeats(false);
>     }
> 
>     @Override
>     public void componentResized(ComponentEvent e) {
>       if( shouldRemoveAddImmediately()){
>         timer.stop();
>         removeAddComponent();
>       }
> 
>       if ( timer.isRunning() ){
>         timer.restart();
>         numberOfTimesRestarted++;
>       } else {
>         timer.start();
>       }
>     }
> 
>     private boolean shouldRemoveAddImmediately(){
>       return numberOfTimesRestarted > TIMER_AVOID_STARVATION;
>     }
> 
>     private void removeAddComponent(){
>       container.remove(component);
>       container.add(component, BorderLayout.CENTER);
> 
>       numberOfTimesRestarted = 0;
> 
>       container.revalidate();
>       container.repaint();
>     }
>   }
> }

If you do so, you'll lose the OpenGL context, won't you?
Comment 8 Robin Stevens 2016-05-13 13:44:35 CEST
(In reply to Julien Gouesse from comment #7)
> (In reply to Robin Stevens from comment #6)
> > The specified workaround of making the GLCanvas visible on a background
> > thread is not always an option in a larger Swing based application. Combined
> > with the fact that deliberately violating the Swing threading rules is
> > asking for problems.
> > 
> > Another workaround that respects the Swing threading rules is to remove and
> > re-add the GLCanvas component from/to the Swing hierarchy on a resize:
> > 
> > import java.awt.BorderLayout;
> > import java.awt.Component;
> > import java.awt.event.ActionEvent;
> > import java.awt.event.ActionListener;
> > import java.awt.event.ComponentAdapter;
> > import java.awt.event.ComponentEvent;
> > 
> > import javax.swing.JPanel;
> > import javax.swing.Timer;
> > 
> > final class TLspOSXViewPositionFixer {
> >   /**
> >    * <p>
> >    *   On OS X, when the heavyweight component gets resized, it is painted
> > in the wrong location.
> >    *   The only workaround we found is to remove and re-add the heavy weight
> > component from/to the Swing hierarchy.
> >    * </p>
> >    *
> >    * <p>
> >    *   This method wraps that component in an extra {@code JPanel} using a
> > {@code BorderLayout},
> >    *   and puts the heavy weight component in the {@code
> > BorderLayout.CENTER}.
> >    *   Each time that {@code JPanel} resizes, the heavy weight component is
> > removed from the panel and re-added.
> >    * </p>
> >    *
> >    * @param aComponent The heavy weight component (for example the GLCanvas)
> >    *
> >    * @return The component to add to the Swing hierarchy
> >    */
> >   static Component convertHostComponent(final Component aComponent ){
> >     final JPanel container = new JPanel(new BorderLayout()){
> > 
> >       @Override
> >       public void repaint(){
> >         super.repaint();
> >         aComponent.repaint();
> >       }
> >     };
> >     container.add(aComponent, BorderLayout.CENTER);
> >     container.addComponentListener(new
> > RemoveReAddHWComponentListener(container, aHostComponent));
> >     return container;
> >   }
> > 
> >   private static class RemoveReAddHWComponentListener extends
> > ComponentAdapter{
> >     /**
> >      * Delay of the timer responsible for removing and re-adding the heavy
> > weight component
> >      */
> >     private static final int TIMER_DELAY = 150;
> >     /**
> >      * The maximum number of times the timer is restarted before it gets
> > executed anyway
> >      */
> >     private static final int TIMER_AVOID_STARVATION = 20;
> > 
> >     private final JPanel container;
> >     private final Component component;
> > 
> >     private final Timer timer = new Timer(TIMER_DELAY, new ActionListener() {
> >       @Override
> >       public void actionPerformed(ActionEvent e) {
> >         removeAddComponent();
> >       }
> >     });
> > 
> >     private int numberOfTimesRestarted = 0;
> > 
> >     private RemoveReAddViewListener(JPanel aContainer, Component aComponent)
> > {
> >       container = aContainer;
> >       component = aViewHostComponent;
> >       timer.setRepeats(false);
> >     }
> > 
> >     @Override
> >     public void componentResized(ComponentEvent e) {
> >       if( shouldRemoveAddImmediately()){
> >         timer.stop();
> >         removeAddComponent();
> >       }
> > 
> >       if ( timer.isRunning() ){
> >         timer.restart();
> >         numberOfTimesRestarted++;
> >       } else {
> >         timer.start();
> >       }
> >     }
> > 
> >     private boolean shouldRemoveAddImmediately(){
> >       return numberOfTimesRestarted > TIMER_AVOID_STARVATION;
> >     }
> > 
> >     private void removeAddComponent(){
> >       container.remove(component);
> >       container.add(component, BorderLayout.CENTER);
> > 
> >       numberOfTimesRestarted = 0;
> > 
> >       container.revalidate();
> >       container.repaint();
> >     }
> >   }
> > }
> 
> If you do so, you'll lose the OpenGL context, won't you?

I have no idea, but it seems to work
Comment 9 Robin Stevens 2016-05-17 14:51:57 CEST
I just created a new build locally from the master branch on Github with patch https://jogamp.org/bugzilla/attachment.cgi?id=786 applied.

After only replacing jogl-all-natives-macosx-universal.jar in my project with the build/jar/jogl-all-natives-macosx-universal.jar from the custom build, this issue goes away.