Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8303950

[macos] Translucent Windows Flicker on Repaint

    XMLWordPrintable

Details

    • x86_64
    • os_x

    Description

      ADDITIONAL SYSTEM INFORMATION :
      Observed on a MacBook Air, M2 2022
      Mac OS 13.2.1
      OpenJDK 19.0.2

      A DESCRIPTION OF THE PROBLEM :
      When we repaint a JComponent in a translucent window: the window can flicker. The severity of the flicker can vary depending on the rate of the repaint.

      Here is a video of the attached test case launched using OpenJDK 19:

      <short clipping link>

      The flickering is sometimes constant.

      Here is a video of the same test case launched from OpenJDK 18.0.1:

      <short clipping link>

      The flickering is reduced, but still very conspicuous.

      I believe this is the same root cause as this ticket:

      https://bugs.openjdk.org/browse/JDK-8209329

      ... but the way the flicker manifests is different in this ticket. I'd argue flickering while resizing is a tolerable (if annoying) bug, but this is more severe: the user is not doing anything and the window can still flicker.

      I agree with that author that the flickering stems from Window#paint(Graphics) calling `gg.fillRect(0, 0, getWidth(), getHeight());`

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached java source code; see if the throbber flickers.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The throbber should animate and the window background color should remain constant.
      ACTUAL -
      The rectangle where the throbber animates often flickers so it is completely transparent. (You can see whatever windows are underneath the translucent window.)

      ---------- BEGIN SOURCE ----------

      import javax.swing.*;
      import javax.swing.border.EmptyBorder;
      import javax.swing.plaf.ComponentUI;
      import javax.swing.plaf.PanelUI;
      import java.awt.*;
      import java.awt.font.FontRenderContext;
      import java.awt.font.GlyphVector;
      import java.awt.geom.AffineTransform;
      import java.awt.geom.Line2D;
      import java.awt.geom.RoundRectangle2D;
      import java.awt.image.*;
      import java.awt.image.renderable.RenderableImage;
      import java.text.AttributedCharacterIterator;
      import java.util.*;

      /**
       * This is a simple translucent JDialog that demonstrates flickering on Mac and a potential work-around.
       * <p>
       * Instructions are shown in the app.
       * </p>
       */
      public class FlickerTest extends JDialog {
          public static void main(String[] args) {
              SwingUtilities.invokeLater(new Runnable() {
                  @Override
                  public void run() {
                      FlickerTest t = new FlickerTest();
                      t.pack();
                      t.setLocationRelativeTo(null);
                      t.setVisible(true);
                  }
              });
          }

          JTextPane instructions = new JTextPane();
          JCheckBox applyWorkaroundCheckbox = new JCheckBox("Apply Work-Around");

          public FlickerTest() {
              instructions.setText("Instructions for Mac\nIn OpenJDK 18: move the mouse in a circle in the middle of this throbber.\nIn OpenJDK 19: Do nothing.\n\nIf the window flickers: this test fails\nIf the window does not flicker: this test passes.");
              instructions.setBorder(new EmptyBorder(10,10,10,10));
              instructions.setOpaque(false);
              instructions.setEditable(false);

              setUndecorated(true);
              JPanel p = new JPanel();
              p.setOpaque(false);
              p.setBorder(new EmptyBorder(10,10,10,10));
              p.setUI(new PanelUI() {
                  @Override
                  public void paint(Graphics g, JComponent c) {
                      Graphics2D g2 = (Graphics2D) g;
                      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                      g2.setColor(new Color(220, 180, 0, 200));
                      g2.fill(new RoundRectangle2D.Double(0,0,c.getWidth(),c.getHeight(),20,20));
                  }
              });
              p.setLayout(new BorderLayout());
              p.add(instructions, BorderLayout.NORTH);
              p.add(new JThrobber(), BorderLayout.CENTER);
              p.add(applyWorkaroundCheckbox, BorderLayout.SOUTH);
              getContentPane().add(p);
              setBackground(new Color(0,0,0,0));
          }

          public Graphics getGraphics() {
              Graphics2D superG = (Graphics2D) super.getGraphics();
              if (applyWorkaroundCheckbox.isSelected()) {
                  return new IgnoreFillRectGraphics2D( superG );
              }
              return superG;
          }
      }

      /**
       * This is a Graphics2D that ignores all calls to {@link #fillRect(int, int, int, int)}
       */
      class IgnoreFillRectGraphics2D extends DelegateGraphics2D {

          public IgnoreFillRectGraphics2D(Graphics2D delegate) {
              super(delegate);
          }

          @Override
          public Graphics create() {
              return new IgnoreFillRectGraphics2D((Graphics2D) delegate.create());
          }

          @Override
          public void fillRect(int x, int y, int width, int height) {
              // intentionally empty
          }
      }

      /**
       * Also known as a "spinny widget" or an "indeterminate progress indicator": this is used to indicate
       * the application is doing something and the user should wait for the task to complete.
       *
       * This FlickerTest demo is designed so this throbber repaints ridiculously fast to showcase the
       * flickering bug; in a real-world case the repaints should be controlled by some sort of timer.
       */
      class JThrobber extends JComponent {
          public JThrobber() {
              setPreferredSize(new Dimension(300, 300));
              setUI(new ThrobberUI());
          }
      }

      class ThrobberUI extends ComponentUI {

          // copied and pasted from another class
          public static void paint(Graphics2D g, float fraction, Color foreground,
                                   int centerX, int centerY, int r1, int r2, float strokeWidth) {

              if (fraction < 0)
                  throw new IllegalArgumentException(
                          "fraction (" + fraction + ") must be within [0, 1]");

              g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                      RenderingHints.VALUE_ANTIALIAS_ON);

              int i = (int) (fraction * 12);

              int red = foreground.getRed();
              int green = foreground.getGreen();
              int blue = foreground.getBlue();
              Color[] colors = new Color[] { new Color(red, green, blue, 255),
                      new Color(red, green, blue, 240),
                      new Color(red, green, blue, 225),
                      new Color(red, green, blue, 200),
                      new Color(red, green, blue, 160),
                      new Color(red, green, blue, 130),
                      new Color(red, green, blue, 115),
                      new Color(red, green, blue, 100),
                      new Color(red, green, blue, 90),
                      new Color(red, green, blue, 80),
                      new Color(red, green, blue, 70),
                      new Color(red, green, blue, 60)

              };

              g.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND,
                      BasicStroke.JOIN_BEVEL));
              double theta;
              Line2D line = new Line2D.Float();
              for (int a = 0; a < colors.length; a++) {
                  g.setColor(colors[(i + a) % colors.length]);
                  theta = -((double) a) / (colors.length) * Math.PI * 2;
                  line.setLine(centerX + r1 * Math.cos(theta),
                          centerY + r1 * Math.sin(theta),
                          centerX + r2 * Math.cos(theta),
                          centerY + r2 * Math.sin(theta));

                  g.draw(line);
              }
          }

          @Override
          public void paint(Graphics g, JComponent c) {
              long elapsed = System.currentTimeMillis() % 1000;
              float f = ((float) elapsed) / 1000f;
              int r1 = (int)(Math.min(c.getWidth(), c.getHeight())/2 * .9);
              int r2 = r1 * 4 / 8;
              Graphics2D g2 = (Graphics2D)g;
              float strokeWidth = r1 / 6f;
              paint( g2, f, Color.gray, c.getWidth() / 2, c.getHeight() /2, r1, r2, strokeWidth);

              c.repaint();
          }
      }

      /**
       * This delegates every method to its {@link #delegate} field. Subclasses must implement their own {@link #create()}
       * that returns the appropriate subclass.
       * <p>
       * (This is a weird/heavy-handed approach to make the Graphics2D act a little more like an interface than a class.)
       * </p>
       */
      abstract class DelegateGraphics2D extends Graphics2D {
          protected final Graphics2D delegate;

          public DelegateGraphics2D(Graphics2D delegate) {
              this.delegate = Objects.requireNonNull(delegate);
          }

          @Override
          public void draw3DRect(int x, int y, int width, int height, boolean raised) {
              delegate.draw3DRect(x, y, width, height, raised);
          }

          @Override
          public void fill3DRect(int x, int y, int width, int height, boolean raised) {
              delegate.fill3DRect(x, y, width, height, raised);
          }

          @Override
          public FontMetrics getFontMetrics() {
              return delegate.getFontMetrics();
          }

          @Override
          public void drawRect(int x, int y, int width, int height) {
              delegate.drawRect(x, y, width, height);
          }

          @Override
          public void drawPolygon(Polygon p) {
              delegate.drawPolygon(p);
          }

          @Override
          public void fillPolygon(Polygon p) {
              delegate.fillPolygon(p);
          }

          @Override
          public void drawChars(char[] data, int offset, int length, int x, int y) {
              delegate.drawChars(data, offset, length, x, y);
          }

          @Override
          public void drawBytes(byte[] data, int offset, int length, int x, int y) {
              delegate.drawBytes(data, offset, length, x, y);
          }

          @Override
          public String toString() {
              return delegate.toString();
          }

          @Override
          public Rectangle getClipRect() {
              return delegate.getClipRect();
          }

          @Override
          public boolean hitClip(int x, int y, int width, int height) {
              return delegate.hitClip(x, y, width, height);
          }

          @Override
          public Rectangle getClipBounds(Rectangle r) {
              return delegate.getClipBounds(r);
          }

          @Override
          public void draw(Shape s) {
              delegate.draw(s);
          }

          @Override
          public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {
              return delegate.drawImage(img, xform, obs);
          }

          @Override
          public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
              delegate.drawImage(img, op, x, y);
          }

          @Override
          public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
              delegate.drawRenderedImage(img, xform);
          }

          @Override
          public void drawRenderableImage(RenderableImage img, AffineTransform xform) {
              delegate.drawRenderableImage(img, xform);
          }

          @Override
          public void drawString(String str, int x, int y) {
              delegate.drawString(str, x, y);
          }

          @Override
          public void drawString(String str, float x, float y) {
              delegate.drawString(str, x, y);
          }

          @Override
          public void drawString(AttributedCharacterIterator iterator, int x, int y) {
              delegate.drawString(iterator, x, y);
          }

          @Override
          public void drawString(AttributedCharacterIterator iterator, float x, float y) {
              delegate.drawString(iterator, x, y);
          }

          @Override
          public void drawGlyphVector(GlyphVector g, float x, float y) {
              delegate.drawGlyphVector(g, x, y);
          }

          @Override
          public void fill(Shape s) {
              delegate.fill(s);
          }

          @Override
          public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
              return delegate.hit(rect, s, onStroke);
          }

          @Override
          public GraphicsConfiguration getDeviceConfiguration() {
              return delegate.getDeviceConfiguration();
          }

          @Override
          public void setComposite(Composite comp) {
              delegate.setComposite(comp);
          }

          @Override
          public void setPaint(Paint paint) {
              delegate.setPaint(paint);
          }

          @Override
          public void setStroke(Stroke s) {
              delegate.setStroke(s);
          }

          @Override
          public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
              delegate.setRenderingHint(hintKey, hintValue);
          }

          @Override
          public Object getRenderingHint(RenderingHints.Key hintKey) {
              return delegate.getRenderingHint(hintKey);
          }

          @Override
          public void setRenderingHints(Map<?, ?> hints) {
              delegate.setRenderingHints(hints);
          }

          @Override
          public void addRenderingHints(Map<?, ?> hints) {
              delegate.addRenderingHints(hints);
          }

          @Override
          public RenderingHints getRenderingHints() {
              return delegate.getRenderingHints();
          }

          @Override
          public void translate(int x, int y) {
              delegate.translate(x, y);
          }

          @Override
          public void translate(double tx, double ty) {
              delegate.translate(tx, ty);
          }

          @Override
          public void rotate(double theta) {
              delegate.rotate(theta);
          }

          @Override
          public void rotate(double theta, double x, double y) {
              delegate.rotate(theta, x, y);
          }

          @Override
          public void scale(double sx, double sy) {
              delegate.scale(sx, sy);
          }

          @Override
          public void shear(double shx, double shy) {
              delegate.shear(shx, shy);
          }

          @Override
          public void transform(AffineTransform Tx) {
              delegate.transform(Tx);
          }

          @Override
          public void setTransform(AffineTransform Tx) {
              delegate.setTransform(Tx);
          }

          @Override
          public AffineTransform getTransform() {
              return delegate.getTransform();
          }

          @Override
          public Paint getPaint() {
              return delegate.getPaint();
          }

          @Override
          public Composite getComposite() {
              return delegate.getComposite();
          }

          @Override
          public void setBackground(Color color) {
              delegate.setBackground(color);
          }

          @Override
          public Color getBackground() {
              return delegate.getBackground();
          }

          @Override
          public Stroke getStroke() {
              return delegate.getStroke();
          }

          @Override
          public void clip(Shape s) {
              delegate.clip(s);
          }

          @Override
          public FontRenderContext getFontRenderContext() {
              return delegate.getFontRenderContext();
          }

          @Override
          public Color getColor() {
              return delegate.getColor();
          }

          @Override
          public void setColor(Color c) {
              delegate.setColor(c);
          }

          @Override
          public void setPaintMode() {
              delegate.setPaintMode();
          }

          @Override
          public void setXORMode(Color c1) {
              delegate.setXORMode(c1);
          }

          @Override
          public Font getFont() {
              return delegate.getFont();
          }

          @Override
          public void setFont(Font font) {
              delegate.setFont(font);
          }

          @Override
          public FontMetrics getFontMetrics(Font f) {
              return delegate.getFontMetrics();
          }

          @Override
          public Rectangle getClipBounds() {
              return delegate.getClipBounds();
          }

          @Override
          public void clipRect(int x, int y, int width, int height) {
              delegate.clipRect(x, y, width, height);
          }

          @Override
          public void setClip(int x, int y, int width, int height) {
              delegate.setClip(x, y, width, height);
          }

          @Override
          public Shape getClip() {
              return delegate.getClip();
          }

          @Override
          public void setClip(Shape clip) {
              delegate.setClip(clip);
          }

          @Override
          public void copyArea(int x, int y, int width, int height, int dx, int dy) {
              delegate.copyArea(x, y, width, height, dx, dy);
          }

          @Override
          public void drawLine(int x1, int y1, int x2, int y2) {
              delegate.drawLine(x1, y1, x2, y2);
          }

          @Override
          public void fillRect(int x, int y, int width, int height) {
              delegate.fillRect(x, y, width, height);
          }

          @Override
          public void clearRect(int x, int y, int width, int height) {
              delegate.clearRect(x, y, width, height);
          }

          @Override
          public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
              delegate.drawRoundRect(x,y,width,height, arcWidth,arcHeight);
          }

          @Override
          public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {
              delegate.fillRoundRect(x,y,width,height, arcWidth,arcHeight);
          }

          @Override
          public void drawOval(int x, int y, int width, int height) {
              delegate.drawOval(x,y,width,height);
          }

          @Override
          public void fillOval(int x, int y, int width, int height) {
              delegate.fillOval(x,y,width,height);
          }

          @Override
          public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
              delegate.drawArc(x,y,width,height,startAngle,arcAngle);
          }

          @Override
          public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
              delegate.fillArc(x,y,width,height,startAngle,arcAngle);
          }

          @Override
          public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
              delegate.drawPolyline(xPoints, yPoints, nPoints);
          }

          @Override
          public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
              delegate.drawPolygon(xPoints, yPoints, nPoints);
          }

          @Override
          public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {

          }

          @Override
          public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
              return delegate.drawImage(img, x, y, observer);
          }

          @Override
          public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
              return delegate.drawImage(img, x, y, width, height, observer);
          }

          @Override
          public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
              return delegate.drawImage(img, x, y, bgcolor, observer);
          }

          @Override
          public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
              return delegate.drawImage(img, x, y, width, height, bgcolor, observer);
          }

          @Override
          public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
              return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
          }

          @Override
          public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) {
              return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer);
          }

          @Override
          public void dispose() {
              delegate.dispose();
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The source code includes a checkbox that demonstrates the following work-around:

      We can override our Dialog's getGraphics() method so it resembles:
      ```
      public Graphics getGraphics() {
          Graphics2D superG = (Graphics2D) super.getGraphics();
          if (applyWorkaroundCheckbox.isSelected()) {
              return new IgnoreFillRectGraphics2D( superG );
          }
          return superG;
      }
      ```

      IgnoreFillRectGraphics2D is a Graphics2D that ignores all calls to `fillRect(..)`.

      My (imperfect) understanding is: Swing's RepaintManager handles double buffering to an offscreen image for us, so all we really need to do with the Graphics2D in Window.paint(Graphics) is impose our buffered offscreen image. So we never need to call `fillRect(..)`, and for whatever reason calling it actually causes harm. (The call to `fillRect(..)` may (?) be important in other contexts; the observation that it's unnecessary is limited to repaints coming from the RepaintManager class.)

      FREQUENCY : always


      Attachments

        Issue Links

          Activity

            People

              Unassigned Unassigned
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

              Dates

                Created:
                Updated: