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

[macos]Transparent Windows Paint Wrong (Opaque, Pixelated) w/o Volatile Buffering

XMLWordPrintable

      ADDITIONAL SYSTEM INFORMATION :
      Mac OS 13.2.1
      OpenJDK 19.0.2

      A DESCRIPTION OF THE PROBLEM :
      When "swing.volatileImageBufferEnabled" is set to "false": transparent windows don't render correctly.

      Specifically:
      1. They render opaque
      2. They render at 100% their virtual size, regardless of the monitor resolution.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached test case.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The window should not have a black border, it should be a little translucent, and the text resolution should match the resolution of your monitor.
      ACTUAL -
      The window has a black border, it is opaque, and the text is rendered as if it is on a 100% resolution monitor.

      Here is a video that clearly shows the black border:
      <short clipping Link>

      The difference in text quality is not as easy to make out.

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

      import javax.swing.*;
      import javax.swing.border.EmptyBorder;
      import javax.swing.plaf.PanelUI;
      import java.awt.*;
      import java.awt.geom.RoundRectangle2D;
      import java.awt.image.*;
      import java.util.*;
      import java.util.List;

      /**
       * This tests how transparent windows and repaint resolution using transparent windows when Swing's
       * volatile image buffering is disabled.
       * <p>
       * Instructions are visible inside the app.
       * </p>
       */
      public class NonVolatileRepaintManagerTest extends JDialog {
          public static void main(String[] args) {
              System.setProperty("swing.volatileImageBufferEnabled", "false");
              SwingUtilities.invokeLater(() -> {
                  NonVolatileRepaintManagerTest t = new NonVolatileRepaintManagerTest();
                  t.pack();
                  t.setLocationRelativeTo(null);
                  t.setVisible(true);
              });
          }

          JTextPane instructions = new JTextPane();
          JCheckBox customRepaintManagerCheckbox = new JCheckBox("Apply Custom RepaintManager");

          public NonVolatileRepaintManagerTest() {
              instructions.setText("Instructions\n\nLook at this window. This test passes if both of these conditions are met:\n\n1. The window does NOT have a black border.\n2. Toggling the checkbox does NOT affect text antialiasing on a high resolution monitor.");
              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(5, 5,c.getWidth()-10,c.getHeight()-10,20,20));
                  }
              });
              p.setLayout(new BorderLayout());
              p.add(instructions, BorderLayout.NORTH);
              p.add(customRepaintManagerCheckbox, BorderLayout.SOUTH);
              getContentPane().add(p);
              setBackground(new Color(0,0,0,0));

              customRepaintManagerCheckbox.addActionListener(e -> {
                  if (customRepaintManagerCheckbox.isSelected()) {
                      RepaintManager.setCurrentManager(new DisabledVolatileImageBufferRepaintManager_workaround());
                  } else {
                      RepaintManager.setCurrentManager(new RepaintManager());
                  }
                  SwingUtilities.invokeLater(() -> repaint());
              });
          }
      }

      class DisabledVolatileImageBufferRepaintManager_workaround extends RepaintManager {
          BufferedImage offscreenBuffer;

          @Override
          public Image getOffscreenBuffer(Component c, int proposedWidth, int proposedHeight) {
              GraphicsConfiguration gc = c.getGraphicsConfiguration();
              if (gc == null) {
                  // can we ever reach this condition?
                  return super.getOffscreenBuffer(c, proposedWidth, proposedHeight);
              }

              Window w = c instanceof Window window ? window : SwingUtilities.getWindowAncestor(c);
              if (w == null || w.isOpaque()) {
                  // opaque windows are what the super class already handles correctly:
                  return super.getOffscreenBuffer(c, proposedWidth, proposedHeight);
              }

              float scalingFactor = (float)Math.sqrt(gc.getDefaultTransform().getDeterminant());
              int pixelWidth = Math.max(1, Math.round(proposedWidth * scalingFactor));
              int pixelHeight = Math.max(1, Math.round(proposedHeight * scalingFactor));

              if (offscreenBuffer == null || offscreenBuffer.getWidth() < pixelWidth || offscreenBuffer.getHeight() < pixelHeight) {

                  // This is copied and pasted from MTLGraphicsConfig#createAcceleratedImage , except we use TRANSLUCENT:

                  ColorModel model = gc.getColorModel(Transparency.TRANSLUCENT);
                  WritableRaster wr = model.createCompatibleWritableRaster(pixelWidth, pixelHeight);
                  offscreenBuffer = new BufferedImage(model, wr,
                          model.isAlphaPremultiplied(), new Hashtable<>());
              }

              BufferedImage subimage = offscreenBuffer.getSubimage(0,0,pixelWidth,pixelHeight);

              if (scalingFactor == 1)
                  return subimage;

              // step 2: it will be pixelated on high-res monitors unless we return a MultiResolutionImage:

              return new ScaledResolutionImage(subimage, proposedWidth, proposedHeight, scalingFactor);
          }
      }

      /**
       * This can be a high-resolution image that presents itself as having a smaller virtual width/height.
       * For example on a 200% resolution monitor this image may act like it's 400x300, but it's actually backed
       * by a 800x600 BufferedImage.
       */
      class ScaledResolutionImage extends Image implements MultiResolutionImage {
          final int width, height;
          final BufferedImage img;
          final float scalingFactor;

          public ScaledResolutionImage(BufferedImage img, int width, int height, float scalingFactor) {
              this.img = Objects.requireNonNull(img);
              this.width = width;
              this.height = height;
              this.scalingFactor = scalingFactor;
          }

          @Override
          public int getWidth(ImageObserver observer) {
              return width;
          }

          @Override
          public int getHeight(ImageObserver observer) {
              return height;
          }

          @Override
          public ImageProducer getSource() {
              return img.getSource();
          }

          @Override
          public Graphics2D getGraphics() {
              Graphics2D g = img.createGraphics();
              g.scale(scalingFactor, scalingFactor);
              return g;
          }

          @Override
          public Object getProperty(String name, ImageObserver observer) {
              return img.getProperty(name, observer);
          }

          @Override
          public Image getResolutionVariant(double destImageWidth, double destImageHeight) {
              return img;
          }

          @Override
          public List<Image> getResolutionVariants() {
              return Arrays.asList(this, img);
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The code above includes a checkbox that activates a workaround.

      Specifically the workaround is:
      We can install a custom RepaintManager that overrides getOffscreenBuffer to return a translucent, high-res image.

      FREQUENCY : always


            abaya Anass Baya
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: