4

I recently made the decision to switch over to a newer version of the JDK from the one I was using. (Specifically from jdk1.8.0_261 to jdk-14.0.2). This upgrade when pretty smoothly, as most features work the way I expected them and I was able to find how to adjust to anything that was not the same.

However, as shown in the title, I came across an issue with a specific application I am writing that uses Swing. My main monitor is a 4k monitor, and I have set the windows system scaling for that monitor to 200%. While every other Swing application being scaled to match that DPI setting is nice, for this specific application I want to disable that scaling and have pixels be drawn 1:1, especially when this Application is distributed.

Is there a way to disable this automatic scaling with a VM argument? Or is there a way to disable the scaling at runtime before any calls are made to the swing library? (Similar to how any changes to the look and feel must be done before any other calls to the library.)

For reference:

Here is the code I use to create my JFrame and to create the graphics object I draw everything onto, which happens in a timed loop outside of this class and the scope of this question.

public class ScreenManager {
    private GraphicsDevice device;
    private final JFrame frame;
    
    private static ScreenManager instance;
    
    public ScreenManager() {
        instance = this;
        
        GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
        this.device = env.getDefaultScreenDevice();
        
        this.frame = new JFrame(game.getGame().gameName());
        this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.frame.setUndecorated(true);
        this.frame.setIgnoreRepaint(true);
        this.frame.setResizable(false);
        
        this.device.setFullScreenWindow(frame);
        this.device.setDisplayMode(device.getDisplayMode());
        
        this.frame.createBufferStrategy(2);
    }

    public Graphics2D getRenderGraphics() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            BufferStrategy strategy = window.getBufferStrategy();
            Graphics2D g = (Graphics2D) strategy.getDrawGraphics();
            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            return g;
        }
        return null;
    }

    public void updateDisplay() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            BufferStrategy strategy = window.getBufferStrategy();
            if (!strategy.contentsLost()) {
                strategy.show();
            }
        }
        Toolkit.getDefaultToolkit().sync();
    }
    // other methods go here
}

Using AffineTransforms would, while welcome advice, not truly help solve this issue, as I need to be able to display at the native resolution of any monitor and already extensively use AffineTransforms on the Graphics object.

EDIT: I have tried System.setProperty("sun.java2d.dpiaware", "false"); as the first line in main with no success. Is the property wrong?

EDIT 2: Addition of more clarity:

My render method looks like this:

private void render(Graphics2D g) {
    // Black out the screen to prevent old stuff from showing
    g.setColor(Color.BLACK);
    g.fillRect(0, 0, ScreenManager.getScreenWidth(), ScreenManager.getScreenHeight());
    
    // Set the transform for zoom to draw the zoomed stuffs
    AffineTransform saveState = g.getTransform();
    AffineTransform cameraTransform = ScreenManager.getInstance().getCamera().getCurrentTransform();
    g.transform(cameraTransform);
    
    // RENDER UPDATEABLE
    this.game.getController().renderGame(g);
    
    // REDNER STATIC GUI
    g.setTransform(saveState);
    // draw mouse cords
    Point mouse = this.mouseHandler.getFrameMousePoint();
    if (mouse != null) {
        Font f = new Font("Consolas", 0, 40);
        Point2D scaledPos;
        try {
            scaledPos = cameraTransform.inverseTransform(mouse, null);
        } catch (NoninvertibleTransformException e) {
            scaledPos = mouse;
            e.printStackTrace();
        }
        String s1 = mouse.toString();
        String s2 = scaledPos.toString();
        Rectangle2D r = f.getMaxCharBounds(g.getFontRenderContext());
        int yOff = (int) r.getHeight();
        Color c = new Color(100, 100, 100, 191);
        g.setColor(c);
        g.fillRect(mouse.x, mouse.y, (int) (r.getWidth() * (s1.length() > s2.length() ? s1.length() : s2.length())), yOff + yOff + yOff);
        g.setColor(Color.WHITE);
        g.setFont(f);
        g.drawString(s1, mouse.x, mouse.y + yOff);
        g.drawString(s2, mouse.x, mouse.y + yOff + yOff);
    }
}

And this method is called here:

Graphics2D g = screenManager.getRenderGraphics();
render(g);
g.dispose();
screenManager.updateDisplay(); // SYNCS SCREEN WITH VSync

I have printed out the AffineTransform that is on the graphics object when it is first created, and that prints like this AffineTransform[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]] which does lead me to conclude that the scaling is done in code. However, the issue is, the things that are rendered with my custom transform ScreenManager.getInstance().getCamera().getCurrentTransform(); are also scaled up by 2, even though I do not contact my transform and set it to mine. (Actually never mind, I realized that I do g.transform on my transform not g.setTransform. I am still including this in the write-up though).

A workaround solution to my rendering problem is to do g.setTransform(AffineTransform.getScaleInstance(1, 1); However, this is more than I want to do and it (after trying it on my code) does not fix the issue with the awt coordinate space. Meaning, The rendering works as desired, but the mouse position does not. When the mouse is in the bottom right corner of my screen, it will give a position that is half of the native resolution of my monitor. This solution does not work for me. Processing time is valuable and the more calculations that must be done to UNDO this feature addition. Here is a link discussing how this would be implemented. I link this to point out the section where it mentions that the developer could do it, but it would take a lot of work on the developers part.

I would still like to know if there is a way to either:

  • Disable the feature with a command-line VM argument
  • Disable the feature with a snippet of code
Joseph Terribile
  • 111
  • 2
  • 10
  • *as I need to be able to display at the native resolution* - I believe Swing handles scaling by setting the scale of an AffineTransform. So you could try replacing the scaling with your own that does no scaling. *and already extensively use AffineTransforms on the Graphics object.* - you should NOT be changing the transform on the Graphics object passed to a painting mehtod. Instead you create a copy of the Graphics object and "concatenate()" your transform to the default transform of the copied Graphics instance. – camickr Jul 29 '20 at 00:48
  • I looked into what you said about swing accomplishing the scaling using AffineTransforms, you are correct there is a scale affine transform. However, on the not about using affine transforms on the graphics object, no swing components are used other than the JFrame in question. The only other elements of swing used are the even listeners for various inputs. So it will not cause issues with other components. Also, this frame ignores repaints, as the engine behind rendering is in a separate part of the code base and independent of Swing. I still prefer an option to turn this off entirely – Joseph Terribile Jul 29 '20 at 02:10
  • Did you try to set the transform of the graphics object you are creating in `getRenderGraphics()`? – weisj Jul 29 '20 at 09:56
  • 1
    When you are not using any other Swing component, i.e. do not depend on the logical coordinate system at all, I don’t get your question. Just setting an identity transformation on the `Graphics2D` at the beginning will give you native resolution. You only have to extract the original scaling values to find out which actual canvas size you have in absolute numbers. When you say you are already using transformations, this is no complication in your code. So why not do this, instead of asking for an implementation specific option that will fail in other JREs? – Holger Jul 29 '20 at 10:48
  • 1
    And when you don’t use any Swing feature, why are you using `JFrame` at all, instead of just a `Frame` or `Window`? – Holger Jul 29 '20 at 10:52

1 Answers1

7

After coming back to this question after a while of working on something else, I have come across the solution I was looking for, mostly by accident.

The solution I was looking for was a Java system property or JVM argument that would disable the DPI awareness of Swing components, meaning that on a system with 200% scaling, the user input space and component rendering would match the native resolution of the screen and not the 200% scaled resolution (eg expecting to read mouse input from and render to a 3840x2160 screen rather than what was happening, where mouse input was capped at 1920x1080).

This solution was discussed in this answer to a similar question. I will restate it here for sake of being complete.

The solution is to pass -Dsun.java2d.uiScale=1 into the VM as a VM argument. This forces the UI scale to be 1, ignoring any system scaling and rendering and gathering mouse input at native resolution. I can also confirm that calling System.setProperty("sun.java2d.uiScale", "1"); before calling any swing class will also produce the result I was looking for.

Joseph Terribile
  • 111
  • 2
  • 10