JDK-8371679 : Pixel snapping issue when using Metal rendering api
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 25,26
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2025-11-10
  • Updated: 2025-11-14
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Description
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 ----------


Comments
Additional Information from submitter: ============================== Short video demonstrating the issue for better understanding <LINK> I've reproduced the issue on the latest MacOs Sequoia using Java OpenJDK 21. I've also tried to workaround the issue for a few hours but finally gave up. It is possible to move the canvas out of the frame and back in. After that the effect is gone but the canvas is also resized. If I call .pack() on the frame the canvas will fit back in but the bug instantly reappears.
14-11-2025

The observations on MacOS: JDK 8: See the attached screenshot JDK 26ea+17: The same as JDK 8's output Impact -> M (Somewhere in-between the extremes) Likelihood -> L (Uncommon cases) Workaround -> M (Somewhere in-between the extremes) Priority -> P4
12-11-2025