From 991ca14279faa7db5d0afe023e666844f5b3b75b Mon Sep 17 00:00:00 2001
From: Phil Race <prr@openjdk.org>
Date: Wed, 9 Jun 2021 20:52:43 +0000
Subject: [PATCH] 8267430: GraphicsDevice.setDisplayMode(REFRESH_RATE_UNKNOWN)
 throws IAE: Unable to set display mode!

Reviewed-by: serb
---
 .../classes/sun/awt/CGraphicsDevice.java      | 62 ++++++++++++++--
 .../native/libawt_lwawt/awt/CGraphicsDevice.m | 12 ++++
 .../DisplayModes/UnknownRefrshRateTest.java   | 72 +++++++++++++++++++
 3 files changed, 142 insertions(+), 4 deletions(-)
 create mode 100644 test/jdk/java/awt/GraphicsDevice/DisplayModes/UnknownRefrshRateTest.java

diff --git a/src/java.desktop/macosx/classes/sun/awt/CGraphicsDevice.java b/src/java.desktop/macosx/classes/sun/awt/CGraphicsDevice.java
index 99c4a0368c8..5a865f92cb6 100644
--- a/src/java.desktop/macosx/classes/sun/awt/CGraphicsDevice.java
+++ b/src/java.desktop/macosx/classes/sun/awt/CGraphicsDevice.java
@@ -34,6 +34,7 @@ import java.awt.Rectangle;
 import java.awt.Window;
 import java.awt.geom.Rectangle2D;
 import java.awt.peer.WindowPeer;
+import java.util.Arrays;
 import java.util.Objects;
 
 import sun.java2d.SunGraphicsEnvironment;
@@ -65,9 +66,11 @@ public final class CGraphicsDevice extends GraphicsDevice
 
     // Save/restore DisplayMode for the Full Screen mode
     private DisplayMode originalMode;
+    private DisplayMode initialMode;
 
     public CGraphicsDevice(final int displayID) {
         this.displayID = displayID;
+        this.initialMode = getDisplayMode();
 
         if (MacOSFlags.isMetalEnabled()) {
             // Try to create MTLGraphicsConfig, if it fails,
@@ -201,6 +204,7 @@ public final class CGraphicsDevice extends GraphicsDevice
     public void invalidate(CGraphicsDevice device) {
         //TODO do we need to restore the full-screen window/modes on old device?
         displayID = device.displayID;
+        initialMode = device.initialMode;
     }
 
     @Override
@@ -307,14 +311,47 @@ public final class CGraphicsDevice extends GraphicsDevice
         return true;
     }
 
+    /* If the modes are the same or the only difference is that
+     * the new mode will match any refresh rate, no need to change.
+     */
+    private boolean isSameMode(final DisplayMode newMode,
+                               final DisplayMode oldMode) {
+
+        return (Objects.equals(newMode, oldMode) ||
+                (newMode.getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN &&
+                 newMode.getWidth() == oldMode.getWidth() &&
+                 newMode.getHeight() == oldMode.getHeight() &&
+                 newMode.getBitDepth() == oldMode.getBitDepth()));
+    }
+
     @Override
     public void setDisplayMode(final DisplayMode dm) {
         if (dm == null) {
             throw new IllegalArgumentException("Invalid display mode");
         }
-        if (!Objects.equals(dm, getDisplayMode())) {
-            nativeSetDisplayMode(displayID, dm.getWidth(), dm.getHeight(),
-                                 dm.getBitDepth(), dm.getRefreshRate());
+        if (!isSameMode(dm, getDisplayMode())) {
+            try {
+                nativeSetDisplayMode(displayID, dm.getWidth(), dm.getHeight(),
+                                    dm.getBitDepth(), dm.getRefreshRate());
+            } catch (Throwable t) {
+                /* In some cases macOS doesn't report the initial mode
+                 * in the list of supported modes.
+                 * If trying to reset to that mode causes an exception
+                 * try one more time to reset using a different API.
+                 * This does not fix everything, such as it doesn't make
+                 * that mode reported and it restores all devices, but
+                 * this seems a better compromise than failing to restore
+                 */
+                if (isSameMode(dm, initialMode)) {
+                    nativeResetDisplayMode();
+                    if (!isSameMode(initialMode, getDisplayMode())) {
+                        throw new IllegalArgumentException(
+                            "Could not reset to initial mode");
+                    }
+                } else {
+                   throw t;
+                }
+            }
         }
     }
 
@@ -325,7 +362,22 @@ public final class CGraphicsDevice extends GraphicsDevice
 
     @Override
     public DisplayMode[] getDisplayModes() {
-        return nativeGetDisplayModes(displayID);
+        DisplayMode[] nativeModes = nativeGetDisplayModes(displayID);
+        boolean match = false;
+        for (DisplayMode mode : nativeModes) {
+            if (initialMode.equals(mode)) {
+                match = true;
+                break;
+            }
+        }
+        if (match) {
+            return nativeModes;
+        } else {
+          int len = nativeModes.length;
+          DisplayMode[] modes = Arrays.copyOf(nativeModes, len+1, DisplayMode[].class);
+          modes[len] = initialMode;
+          return modes;
+        }
     }
 
     public static boolean usingMetalPipeline() {
@@ -345,6 +397,8 @@ public final class CGraphicsDevice extends GraphicsDevice
 
     private static native double nativeGetScaleFactor(int displayID);
 
+    private static native void nativeResetDisplayMode();
+
     private static native void nativeSetDisplayMode(int displayID, int w, int h, int bpp, int refrate);
 
     private static native DisplayMode nativeGetDisplayMode(int displayID);
diff --git a/src/java.desktop/macosx/native/libawt_lwawt/awt/CGraphicsDevice.m b/src/java.desktop/macosx/native/libawt_lwawt/awt/CGraphicsDevice.m
index 4ac03def230..9965cd5df93 100644
--- a/src/java.desktop/macosx/native/libawt_lwawt/awt/CGraphicsDevice.m
+++ b/src/java.desktop/macosx/native/libawt_lwawt/awt/CGraphicsDevice.m
@@ -261,6 +261,18 @@ JNI_COCOA_EXIT(env);
     return ret;
 }
 
+/*
+ * Class:     sun_awt_CGraphicsDevice
+ * Method:    nativeResetDisplayMode
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL
+Java_sun_awt_CGraphicsDevice_nativeResetDisplayMode
+(JNIEnv *env, jclass class)
+{
+    CGRestorePermanentDisplayConfiguration();
+}
+
 /*
  * Class:     sun_awt_CGraphicsDevice
  * Method:    nativeSetDisplayMode
diff --git a/test/jdk/java/awt/GraphicsDevice/DisplayModes/UnknownRefrshRateTest.java b/test/jdk/java/awt/GraphicsDevice/DisplayModes/UnknownRefrshRateTest.java
new file mode 100644
index 00000000000..5679f81b7da
--- /dev/null
+++ b/test/jdk/java/awt/GraphicsDevice/DisplayModes/UnknownRefrshRateTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8267430
+ * @key headful
+ * @summary verify setting a display mode with unknow refresh rate works
+ */
+
+import java.awt.DisplayMode;
+import java.awt.GraphicsDevice;
+import java.awt.GraphicsEnvironment;
+
+public class UnknownRefrshRateTest {
+
+    public static void main(String[] args) throws Exception {
+
+        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+        GraphicsDevice[] devices = ge.getScreenDevices();
+
+        for (GraphicsDevice d : devices) {
+
+            if (!d.isDisplayChangeSupported()) {
+                continue;
+            }
+            DisplayMode odm = d.getDisplayMode();
+            System.out.println("device=" + d + " original mode=" + odm);
+
+            DisplayMode[] modes = d.getDisplayModes();
+            System.out.println("There are " + modes.length + " modes.");
+            try {
+                for (int i=0; i<modes.length; i++) {
+                    DisplayMode mode = modes[i];
+                    System.out.println("copying from mode " + i + " : " + mode);
+                    int w = mode.getWidth();
+                    int h = mode.getHeight();
+                    int bpp = mode.getBitDepth();
+                    int refRate = DisplayMode.REFRESH_RATE_UNKNOWN;
+                    DisplayMode newMode = new DisplayMode(w, h, bpp, refRate);
+                    d.setDisplayMode(newMode);
+                    Thread.sleep(2000);
+                    System.out.println("set " + d.getDisplayMode());
+                 }
+             } finally {
+                 System.out.println("restoring original mode"+odm);
+                 d.setDisplayMode(odm);
+                 Thread.sleep(10000);
+             }
+       }
+    }
+}