-
Bug
-
Resolution: Unresolved
-
P3
-
8, 11, 17, 19, 20, 21
-
x86_64
-
os_x
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
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
- links to
-
Review openjdk/jdk/13196
-
Review(master) openjdk/jdk/23430