ADDITIONAL SYSTEM INFORMATION :
MacOs Tahoe 26.0.1
OpenJDK 25.0.1
A DESCRIPTION OF THE PROBLEM :
When running a Java AWT application on macOS using JDK 25, visual artifacts appear on the borders of a Canvas or Frame when drawing scaled images using nearest-neighbor interpolation (the default or when explicitly set). The bug manifests as incorrect "pixel snapping" behavior, where partial pixels at the boundary of the rendering surface are drawn as full pixels, creating visible seams or misaligned edges.
This issue specifically occurs with the new -Dsun.java2d.metal=true pipeline (which is the default in newer JDKs on macOS) and does not occur when using bilinear interpolation or when falling back to the older OpenGL pipeline.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Use macOS (exact version/hardware might vary, likely Apple Silicon).
2. Install OpenJDK 25.
3. Create an AWT application with a Canvas that draws a scaled image using Graphics.drawImage().
4. Ensure nearest neighbor interpolation is used (default behavior or explicitly set RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR).
4. Run the application using the default Metal renderer (no special flags, or -Dsun.java2d.metal=true).
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Images scaled with nearest neighbor interpolation should maintain sharp pixel edges without visual artifacts, rendering partial pixels correctly (e.g., clipping cleanly at the canvas boundary).
ACTUAL -
Images rendered on the border of the canvas exhibit incorrect pixel snapping (e.g., an 8-pixel image scaled 6 times displays 6 full pixels at a time, creating a jerky or misaligned edge effect) specifically with the Metal renderer's nearest-neighbor implementation.
---------- BEGIN SOURCE ----------
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public class DemoApp extends Canvas {
public static void main(String[] args) {
System.setProperty("sun.java2d.metal", "true");
BufferedImage spriteImage = createTestSprite(32, 32);
final BufferedImage finalImage = spriteImage;
final int scaleFactor = 12;
JFrame frame = new JFrame("Resize window to experience bug");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
BuggyCanvas canvas = new BuggyCanvas(finalImage, scaleFactor);
frame.add(canvas);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
static class BuggyCanvas extends JPanel {
private final BufferedImage sprite;
private final int scale;
public BuggyCanvas(BufferedImage sprite, int scale) {
this.sprite = sprite;
this.scale = scale;
int width = sprite.getWidth() * scale;
int height = sprite.getHeight() * scale;
setPreferredSize(new Dimension(width, height));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
AffineTransform tx = new AffineTransform();
tx.scale(20, 20);
tx.translate(15, 20);
g2d.setTransform(tx);
g2d.drawImage(sprite, 10, 0, null);
}
}
private static BufferedImage createTestSprite(int w, int h) {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int color = ((x + y) % 2 == 0) ? 0xFF0000 : 0x0000FF;
if (x == 0 || x == w - 1 || y == 0 || y == h - 1) {
color = 0xFFFFFF; // White
}
img.setRGB(x, y, color);
}
}
return img;
}
}
---------- END SOURCE ----------