Introducing Display Sythetic modes - used for app request refresh rate/resolution selection

Synthetic modes are used for vrr displays to hide normal speed modes for applications.
When application request synthetic mode, instead voting for this mode, DisplayModeDirector will vote for RenderRate and Size

Bug: b/338183249
Test: atest DisplayServiceTests
Change-Id: I374f137fefd2ba43ab72507db68043d840334835
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 4475418..4244771 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1223,7 +1223,7 @@
     public Mode[] getSupportedModes() {
         synchronized (mLock) {
             updateDisplayInfoLocked();
-            final Display.Mode[] modes = mDisplayInfo.supportedModes;
+            final Display.Mode[] modes = mDisplayInfo.appsSupportedModes;
             return Arrays.copyOf(modes, modes.length);
         }
     }
@@ -2206,6 +2206,7 @@
         @NonNull
         @HdrCapabilities.HdrType
         private final int[] mSupportedHdrTypes;
+        private final boolean mIsSynthetic;
 
         /**
          * @hide
@@ -2219,13 +2220,6 @@
         /**
          * @hide
          */
-        public Mode(int width, int height, float refreshRate, float vsyncRate) {
-            this(INVALID_MODE_ID, width, height, refreshRate, vsyncRate, new float[0], new int[0]);
-        }
-
-        /**
-         * @hide
-         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         public Mode(int modeId, int width, int height, float refreshRate) {
             this(modeId, width, height, refreshRate, refreshRate, new float[0], new int[0]);
@@ -2246,11 +2240,21 @@
          */
         public Mode(int modeId, int width, int height, float refreshRate, float vsyncRate,
                 float[] alternativeRefreshRates, @HdrCapabilities.HdrType int[] supportedHdrTypes) {
+            this(modeId, width, height, refreshRate, vsyncRate, false, alternativeRefreshRates,
+                    supportedHdrTypes);
+        }
+        /**
+         * @hide
+         */
+        public Mode(int modeId, int width, int height, float refreshRate, float vsyncRate,
+                boolean isSynthetic, float[] alternativeRefreshRates,
+                @HdrCapabilities.HdrType int[] supportedHdrTypes) {
             mModeId = modeId;
             mWidth = width;
             mHeight = height;
             mPeakRefreshRate = refreshRate;
             mVsyncRate = vsyncRate;
+            mIsSynthetic = isSynthetic;
             mAlternativeRefreshRates =
                     Arrays.copyOf(alternativeRefreshRates, alternativeRefreshRates.length);
             Arrays.sort(mAlternativeRefreshRates);
@@ -2315,6 +2319,15 @@
         }
 
         /**
+         * Returns true if mode is synthetic and does not have corresponding
+         * SurfaceControl.DisplayMode
+         * @hide
+         */
+        public boolean isSynthetic() {
+            return mIsSynthetic;
+        }
+
+        /**
          * Returns an array of refresh rates which can be switched to seamlessly.
          * <p>
          * A seamless switch is one without visual interruptions, such as a black screen for
@@ -2449,6 +2462,7 @@
                     .append(", height=").append(mHeight)
                     .append(", fps=").append(mPeakRefreshRate)
                     .append(", vsync=").append(mVsyncRate)
+                    .append(", synthetic=").append(mIsSynthetic)
                     .append(", alternativeRefreshRates=")
                     .append(Arrays.toString(mAlternativeRefreshRates))
                     .append(", supportedHdrTypes=")
@@ -2464,7 +2478,7 @@
 
         private Mode(Parcel in) {
             this(in.readInt(), in.readInt(), in.readInt(), in.readFloat(), in.readFloat(),
-                    in.createFloatArray(), in.createIntArray());
+                    in.readBoolean(), in.createFloatArray(), in.createIntArray());
         }
 
         @Override
@@ -2474,6 +2488,7 @@
             out.writeInt(mHeight);
             out.writeFloat(mPeakRefreshRate);
             out.writeFloat(mVsyncRate);
+            out.writeBoolean(mIsSynthetic);
             out.writeFloatArray(mAlternativeRefreshRates);
             out.writeIntArray(mSupportedHdrTypes);
         }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 5654bc1..da86e2d 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -211,6 +211,12 @@
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
 
+    /**
+     * The supported modes that will be exposed externally.
+     * Might have different set of modes that supportedModes for VRR displays
+     */
+    public Display.Mode[] appsSupportedModes = Display.Mode.EMPTY_ARRAY;
+
     /** The active color mode. */
     public int colorMode;
 
@@ -429,6 +435,7 @@
                 && defaultModeId == other.defaultModeId
                 && userPreferredModeId == other.userPreferredModeId
                 && Arrays.equals(supportedModes, other.supportedModes)
+                && Arrays.equals(appsSupportedModes, other.appsSupportedModes)
                 && colorMode == other.colorMode
                 && Arrays.equals(supportedColorModes, other.supportedColorModes)
                 && Objects.equals(hdrCapabilities, other.hdrCapabilities)
@@ -488,6 +495,8 @@
         defaultModeId = other.defaultModeId;
         userPreferredModeId = other.userPreferredModeId;
         supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
+        appsSupportedModes = Arrays.copyOf(
+                other.appsSupportedModes, other.appsSupportedModes.length);
         colorMode = other.colorMode;
         supportedColorModes = Arrays.copyOf(
                 other.supportedColorModes, other.supportedColorModes.length);
@@ -545,6 +554,11 @@
         for (int i = 0; i < nModes; i++) {
             supportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
         }
+        int nAppModes = source.readInt();
+        appsSupportedModes = new Display.Mode[nAppModes];
+        for (int i = 0; i < nAppModes; i++) {
+            appsSupportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
+        }
         colorMode = source.readInt();
         int nColorModes = source.readInt();
         supportedColorModes = new int[nColorModes];
@@ -611,6 +625,10 @@
         for (int i = 0; i < supportedModes.length; i++) {
             supportedModes[i].writeToParcel(dest, flags);
         }
+        dest.writeInt(appsSupportedModes.length);
+        for (int i = 0; i < appsSupportedModes.length; i++) {
+            appsSupportedModes[i].writeToParcel(dest, flags);
+        }
         dest.writeInt(colorMode);
         dest.writeInt(supportedColorModes.length);
         for (int i = 0; i < supportedColorModes.length; i++) {
@@ -849,8 +867,10 @@
         sb.append(defaultModeId);
         sb.append(", userPreferredModeId ");
         sb.append(userPreferredModeId);
-        sb.append(", modes ");
+        sb.append(", supportedModes ");
         sb.append(Arrays.toString(supportedModes));
+        sb.append(", appsSupportedModes ");
+        sb.append(Arrays.toString(appsSupportedModes));
         sb.append(", hdrCapabilities ");
         sb.append(hdrCapabilities);
         sb.append(", userDisabledHdrTypes ");
diff --git a/services/core/java/com/android/server/display/DisplayAdapter.java b/services/core/java/com/android/server/display/DisplayAdapter.java
index c26118e..5690a9e 100644
--- a/services/core/java/com/android/server/display/DisplayAdapter.java
+++ b/services/core/java/com/android/server/display/DisplayAdapter.java
@@ -135,7 +135,7 @@
             float[] alternativeRefreshRates,
             @Display.HdrCapabilities.HdrType int[] supportedHdrTypes) {
         return new Display.Mode(NEXT_DISPLAY_MODE_ID.getAndIncrement(), width, height, refreshRate,
-                vsyncRate, alternativeRefreshRates, supportedHdrTypes);
+                vsyncRate, false, alternativeRefreshRates, supportedHdrTypes);
     }
 
     public interface Listener {
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 189e366..5d55d190 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -35,6 +35,7 @@
 
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
@@ -408,7 +409,8 @@
      *
      * @param deviceRepo Repository of active {@link DisplayDevice}s.
      */
-    public void updateLocked(DisplayDeviceRepository deviceRepo) {
+    public void updateLocked(DisplayDeviceRepository deviceRepo,
+            SyntheticModeManager syntheticModeManager) {
         // Nothing to update if already invalid.
         if (mPrimaryDisplayDevice == null) {
             return;
@@ -426,6 +428,7 @@
         // logical display that they are sharing.  (eg. Adjust size for pixel-perfect
         // mirroring over HDMI.)
         DisplayDeviceInfo deviceInfo = mPrimaryDisplayDevice.getDisplayDeviceInfoLocked();
+        DisplayDeviceConfig config = mPrimaryDisplayDevice.getDisplayDeviceConfig();
         if (!Objects.equals(mPrimaryDisplayDeviceInfo, deviceInfo) || mDirty) {
             mBaseDisplayInfo.layerStack = mLayerStack;
             mBaseDisplayInfo.flags = 0;
@@ -507,6 +510,9 @@
             mBaseDisplayInfo.userPreferredModeId = deviceInfo.userPreferredModeId;
             mBaseDisplayInfo.supportedModes = Arrays.copyOf(
                     deviceInfo.supportedModes, deviceInfo.supportedModes.length);
+            mBaseDisplayInfo.appsSupportedModes = syntheticModeManager.createAppSupportedModes(
+                    config, mBaseDisplayInfo.supportedModes
+            );
             mBaseDisplayInfo.colorMode = deviceInfo.colorMode;
             mBaseDisplayInfo.supportedColorModes = Arrays.copyOf(
                     deviceInfo.supportedColorModes,
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index bca53cf..01485cb 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -45,6 +45,7 @@
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.display.utils.DebugUtils;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
@@ -204,6 +205,7 @@
     private boolean mBootCompleted = false;
     private boolean mInteractive;
     private final DisplayManagerFlags mFlags;
+    private final SyntheticModeManager mSyntheticModeManager;
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
             FoldGracePeriodProvider foldGracePeriodProvider,
@@ -213,7 +215,8 @@
         this(context, foldSettingProvider, foldGracePeriodProvider, repo, listener, syncRoot,
                 handler,
                 new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY
-                        : sNextNonDefaultDisplayId++, flags), flags);
+                        : sNextNonDefaultDisplayId++, flags), flags,
+                new SyntheticModeManager(flags));
     }
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
@@ -221,7 +224,7 @@
             @NonNull DisplayDeviceRepository repo,
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
             @NonNull Handler handler, @NonNull DeviceStateToLayoutMap deviceStateToLayoutMap,
-            DisplayManagerFlags flags) {
+            DisplayManagerFlags flags, SyntheticModeManager syntheticModeManager) {
         mSyncRoot = syncRoot;
         mPowerManager = context.getSystemService(PowerManager.class);
         mInteractive = mPowerManager.isInteractive();
@@ -241,6 +244,7 @@
         mDisplayDeviceRepo.addListener(this);
         mDeviceStateToLayoutMap = deviceStateToLayoutMap;
         mFlags = flags;
+        mSyntheticModeManager = syntheticModeManager;
     }
 
     @Override
@@ -737,7 +741,7 @@
             mTempDisplayInfo.copyFrom(display.getDisplayInfoLocked());
             display.getNonOverrideDisplayInfoLocked(mTempNonOverrideDisplayInfo);
 
-            display.updateLocked(mDisplayDeviceRepo);
+            display.updateLocked(mDisplayDeviceRepo, mSyntheticModeManager);
             final DisplayInfo newDisplayInfo = display.getDisplayInfoLocked();
             final int updateState = mUpdatedLogicalDisplays.get(displayId, UPDATE_STATE_NEW);
             final boolean wasPreviouslyUpdated = updateState != UPDATE_STATE_NEW;
@@ -1177,7 +1181,7 @@
         final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device,
                 mFlags.isPixelAnisotropyCorrectionInLogicalDisplayEnabled(),
                 mFlags.isAlwaysRotateDisplayDeviceEnabled());
-        display.updateLocked(mDisplayDeviceRepo);
+        display.updateLocked(mDisplayDeviceRepo, mSyntheticModeManager);
 
         final DisplayInfo info = display.getDisplayInfoLocked();
         if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index cd07f5a..a5414fc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -164,6 +164,11 @@
             Flags::ignoreAppPreferredRefreshRateRequest
     );
 
+    private final FlagState mSynthetic60hzModes = new FlagState(
+            Flags.FLAG_ENABLE_SYNTHETIC_60HZ_MODES,
+            Flags::enableSynthetic60hzModes
+    );
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -333,6 +338,10 @@
         return mIgnoreAppPreferredRefreshRate.isEnabled();
     }
 
+    public boolean isSynthetic60HzModesEnabled() {
+        return mSynthetic60hzModes.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -365,6 +374,8 @@
         pw.println(" " + mResolutionBackupRestore);
         pw.println(" " + mUseFusionProxSensor);
         pw.println(" " + mPeakRefreshRatePhysicalLimit);
+        pw.println(" " + mIgnoreAppPreferredRefreshRate);
+        pw.println(" " + mSynthetic60hzModes);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a15a8e8..316b6db 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -266,3 +266,15 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "enable_synthetic_60hz_modes"
+    namespace: "display_manager"
+    description: "Feature flag for DisplayManager to enable synthetic 60Hz modes for vrr displays"
+    bug: "338183249"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 846ee23..e20ac73 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -147,6 +147,9 @@
 
     // A map from the display ID to the supported modes on that display.
     private SparseArray<Display.Mode[]> mSupportedModesByDisplay;
+    // A map from the display ID to the app supported modes on that display, might be different from
+    // mSupportedModesByDisplay for VRR displays, used in app mode requests.
+    private SparseArray<Display.Mode[]> mAppSupportedModesByDisplay;
     // A map from the display ID to the default mode of that display.
     private SparseArray<Display.Mode> mDefaultModeByDisplay;
     // a map from display id to display device config
@@ -222,6 +225,7 @@
         mVotesStatsReporter = injector.getVotesStatsReporter(
                 displayManagerFlags.isRefreshRateVotingTelemetryEnabled());
         mSupportedModesByDisplay = new SparseArray<>();
+        mAppSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
         mAppRequestObserver = new AppRequestObserver(displayManagerFlags);
         mConfigParameterProvider = new DeviceConfigParameterProvider(injector.getDeviceConfig());
@@ -573,6 +577,12 @@
                 final Display.Mode[] modes = mSupportedModesByDisplay.valueAt(i);
                 pw.println("    " + id + " -> " + Arrays.toString(modes));
             }
+            pw.println("  mAppSupportedModesByDisplay:");
+            for (int i = 0; i < mAppSupportedModesByDisplay.size(); i++) {
+                final int id = mAppSupportedModesByDisplay.keyAt(i);
+                final Display.Mode[] modes = mAppSupportedModesByDisplay.valueAt(i);
+                pw.println("    " + id + " -> " + Arrays.toString(modes));
+            }
             pw.println("  mDefaultModeByDisplay:");
             for (int i = 0; i < mDefaultModeByDisplay.size(); i++) {
                 final int id = mDefaultModeByDisplay.keyAt(i);
@@ -638,6 +648,11 @@
     }
 
     @VisibleForTesting
+    void injectAppSupportedModesByDisplay(SparseArray<Display.Mode[]> appSupportedModesByDisplay) {
+        mAppSupportedModesByDisplay = appSupportedModesByDisplay;
+    }
+
+    @VisibleForTesting
     void injectDefaultModeByDisplay(SparseArray<Display.Mode> defaultModeByDisplay) {
         mDefaultModeByDisplay = defaultModeByDisplay;
     }
@@ -1276,7 +1291,7 @@
             Display.Mode[] modes;
             Display.Mode defaultMode;
             synchronized (mLock) {
-                modes = mSupportedModesByDisplay.get(displayId);
+                modes = mAppSupportedModesByDisplay.get(displayId);
                 defaultMode = mDefaultModeByDisplay.get(displayId);
             }
             for (int i = 0; i < modes.length; i++) {
@@ -1289,7 +1304,7 @@
         }
 
         private void setAppRequestedModeLocked(int displayId, int modeId) {
-            final Display.Mode requestedMode = findModeByIdLocked(displayId, modeId);
+            final Display.Mode requestedMode = findAppModeByIdLocked(displayId, modeId);
             if (Objects.equals(requestedMode, mAppRequestedModeByDisplay.get(displayId))) {
                 return;
             }
@@ -1297,10 +1312,17 @@
             final Vote sizeVote;
             if (requestedMode != null) {
                 mAppRequestedModeByDisplay.put(displayId, requestedMode);
-                baseModeRefreshRateVote =
-                        Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
                 sizeVote = Vote.forSize(requestedMode.getPhysicalWidth(),
                         requestedMode.getPhysicalHeight());
+                if (requestedMode.isSynthetic()) {
+                    // TODO: for synthetic mode we should not limit frame rate, we must ensure
+                    // that frame rate is reachable within other Votes constraints
+                    baseModeRefreshRateVote = Vote.forRenderFrameRates(
+                            requestedMode.getRefreshRate(), requestedMode.getRefreshRate());
+                } else {
+                    baseModeRefreshRateVote =
+                            Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
+                }
             } else {
                 mAppRequestedModeByDisplay.remove(displayId);
                 baseModeRefreshRateVote = null;
@@ -1344,8 +1366,8 @@
                     vote);
         }
 
-        private Display.Mode findModeByIdLocked(int displayId, int modeId) {
-            Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+        private Display.Mode findAppModeByIdLocked(int displayId, int modeId) {
+            Display.Mode[] modes = mAppSupportedModesByDisplay.get(displayId);
             if (modes == null) {
                 return null;
             }
@@ -1424,12 +1446,14 @@
 
             // Populate existing displays
             SparseArray<Display.Mode[]> modes = new SparseArray<>();
+            SparseArray<Display.Mode[]> appModes = new SparseArray<>();
             SparseArray<Display.Mode> defaultModes = new SparseArray<>();
             Display[] displays = mInjector.getDisplays();
             for (Display d : displays) {
                 final int displayId = d.getDisplayId();
                 DisplayInfo info = getDisplayInfo(displayId);
                 modes.put(displayId, info.supportedModes);
+                appModes.put(displayId, info.appsSupportedModes);
                 defaultModes.put(displayId, info.getDefaultMode());
             }
             DisplayDeviceConfig defaultDisplayConfig = mDisplayDeviceConfigProvider
@@ -1438,6 +1462,7 @@
                 final int size = modes.size();
                 for (int i = 0; i < size; i++) {
                     mSupportedModesByDisplay.put(modes.keyAt(i), modes.valueAt(i));
+                    mAppSupportedModesByDisplay.put(appModes.keyAt(i), appModes.valueAt(i));
                     mDefaultModeByDisplay.put(defaultModes.keyAt(i), defaultModes.valueAt(i));
                 }
                 mDisplayDeviceConfigByDisplay.put(Display.DEFAULT_DISPLAY, defaultDisplayConfig);
@@ -1459,6 +1484,7 @@
         public void onDisplayRemoved(int displayId) {
             synchronized (mLock) {
                 mSupportedModesByDisplay.remove(displayId);
+                mAppSupportedModesByDisplay.remove(displayId);
                 mDefaultModeByDisplay.remove(displayId);
                 mDisplayDeviceConfigByDisplay.remove(displayId);
                 mSettingsObserver.removeRefreshRateSetting(displayId);
@@ -1619,6 +1645,11 @@
                     mSupportedModesByDisplay.put(displayId, info.supportedModes);
                     changed = true;
                 }
+                if (!Arrays.equals(mAppSupportedModesByDisplay.get(displayId),
+                        info.appsSupportedModes)) {
+                    mAppSupportedModesByDisplay.put(displayId, info.appsSupportedModes);
+                    changed = true;
+                }
                 if (!Objects.equals(mDefaultModeByDisplay.get(displayId), info.getDefaultMode())) {
                     changed = true;
                     mDefaultModeByDisplay.put(displayId, info.getDefaultMode());
diff --git a/services/core/java/com/android/server/display/mode/SyntheticModeManager.java b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
new file mode 100644
index 0000000..5b6bbc5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+import android.util.Size;
+import android.view.Display;
+
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.feature.DisplayManagerFlags;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * When selected by app synthetic modes will only affect render rate switch rather than mode switch
+ */
+public class SyntheticModeManager {
+    private static final float FLOAT_TOLERANCE = 0.01f;
+    private static final float SYNTHETIC_MODE_HIGH_BOUNDARY = 60f + FLOAT_TOLERANCE;
+
+    private final boolean mSynthetic60HzModesEnabled;
+
+    public SyntheticModeManager(DisplayManagerFlags flags) {
+        mSynthetic60HzModesEnabled = flags.isSynthetic60HzModesEnabled();
+    }
+
+    /**
+     * creates display supportedModes array, exposed to applications
+     */
+    public Display.Mode[] createAppSupportedModes(DisplayDeviceConfig config,
+            Display.Mode[] modes) {
+        if (!config.isVrrSupportEnabled() || !mSynthetic60HzModesEnabled) {
+            return modes;
+        }
+        List<Display.Mode> appSupportedModes = new ArrayList<>();
+        Map<Size, int[]> sizes = new LinkedHashMap<>();
+        int nextModeId = 0;
+        // exclude "real" 60Hz modes and below for VRR displays,
+        // they will be replaced with synthetic 60Hz mode
+        // for VRR display there should be "real" mode with rr > 60Hz
+        for (Display.Mode mode : modes) {
+            if (mode.getRefreshRate() > SYNTHETIC_MODE_HIGH_BOUNDARY) {
+                appSupportedModes.add(mode);
+            }
+            if (mode.getModeId() > nextModeId) {
+                nextModeId = mode.getModeId();
+            }
+
+            float divisor = mode.getVsyncRate() / 60f;
+            boolean is60HzAchievable = Math.abs(divisor - Math.round(divisor)) < FLOAT_TOLERANCE;
+            if (is60HzAchievable) {
+                sizes.put(new Size(mode.getPhysicalWidth(), mode.getPhysicalHeight()),
+                        mode.getSupportedHdrTypes());
+            }
+        }
+        // even if VRR display does not have 60Hz mode, we are still adding synthetic 60Hz mode
+        // for each screen size
+        // vsync rate, alternativeRates and hdrTypes  are not important for synthetic mode,
+        // only refreshRate and size are used for DisplayModeDirector votes.
+        for (Map.Entry<Size, int[]> entry: sizes.entrySet()) {
+            nextModeId++;
+            Size size = entry.getKey();
+            int[] hdrTypes = entry.getValue();
+            appSupportedModes.add(
+                    new Display.Mode(nextModeId, size.getWidth(), size.getHeight(), 60f, 60f, true,
+                            new float[0], hdrTypes));
+        }
+        Display.Mode[] appSupportedModesArr = new Display.Mode[appSupportedModes.size()];
+        return appSupportedModes.toArray(appSupportedModesArr);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index ca5f26a..125eb2a 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -28,7 +28,6 @@
 import android.graphics.Rect;
 import android.os.Message;
 import android.os.Trace;
-import android.util.Log;
 import android.util.Slog;
 import android.view.DisplayInfo;
 import android.window.DisplayAreaInfo;
@@ -391,6 +390,7 @@
                 || first.defaultModeId != second.defaultModeId
                 || first.userPreferredModeId != second.userPreferredModeId
                 || !Arrays.equals(first.supportedModes, second.supportedModes)
+                || !Arrays.equals(first.appsSupportedModes, second.appsSupportedModes)
                 || first.colorMode != second.colorMode
                 || !Arrays.equals(first.supportedColorModes, second.supportedColorModes)
                 || !Objects.equals(first.hdrCapabilities, second.hdrCapabilities)
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index 03574029..8cab7d9 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -275,7 +275,7 @@
         if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
             final int preferredModeId = w.mAttrs.preferredDisplayModeId;
             if (preferredModeId > 0) {
-                for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                for (Display.Mode mode : mDisplayInfo.appsSupportedModes) {
                     if (preferredModeId == mode.getModeId()) {
                         return w.mFrameRateVote.update(mode.getRefreshRate(),
                                 Surface.FRAME_RATE_COMPATIBILITY_EXACT,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 1a03e78..6d138c5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -50,6 +50,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -80,10 +81,10 @@
 
 import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.util.test.LocalServiceKeeperRule;
-import com.android.server.LocalServices;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
 
@@ -91,6 +92,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -141,6 +143,8 @@
     @Mock DisplayManagerFlags mFlagsMock;
     @Mock DisplayAdapter mDisplayAdapterMock;
     @Mock WindowManagerPolicy mWindowManagerPolicy;
+    @Mock
+    SyntheticModeManager mSyntheticModeManagerMock;
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
     @Captor ArgumentCaptor<Integer> mDisplayEventCaptor;
@@ -196,6 +200,8 @@
         when(mResourcesMock.getIntArray(
                 com.android.internal.R.array.config_deviceStatesOnWhichToSleep))
                 .thenReturn(new int[]{0});
+        when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any())).thenAnswer(
+                AdditionalAnswers.returnsSecondArg());
 
         when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false);
         mLooper = new TestLooper();
@@ -204,7 +210,7 @@
                 mFoldGracePeriodProvider,
                 mDisplayDeviceRepo,
                 mListenerMock, new DisplayManagerService.SyncRoot(), mHandler,
-                mDeviceStateToLayoutMapSpy, mFlagsMock);
+                mDeviceStateToLayoutMapSpy, mFlagsMock, mSyntheticModeManagerMock);
         mLogicalDisplayMapper.onWindowManagerReady();
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index 779445e..8936f06 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -40,9 +41,11 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.AdditionalAnswers;
 
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -54,6 +57,7 @@
     private static final int DISPLAY_WIDTH = 100;
     private static final int DISPLAY_HEIGHT = 200;
     private static final int MODE_ID = 1;
+    private static final int OTHER_MODE_ID = 2;
 
     private LogicalDisplay mLogicalDisplay;
     private DisplayDevice mDisplayDevice;
@@ -61,6 +65,7 @@
     private Context mContext;
     private IBinder mDisplayToken;
     private DisplayDeviceRepository mDeviceRepo;
+    private SyntheticModeManager mSyntheticModeManager;
     private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
 
     @Before
@@ -71,6 +76,7 @@
         mDisplayAdapter = mock(DisplayAdapter.class);
         mContext = mock(Context.class);
         mDisplayToken = mock(IBinder.class);
+        mSyntheticModeManager = mock(SyntheticModeManager.class);
         mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
 
         mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo());
@@ -81,6 +87,8 @@
         mDisplayDeviceInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID,
                 DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 60)};
         when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(mDisplayDeviceInfo);
+        when(mSyntheticModeManager.createAppSupportedModes(any(), any())).thenAnswer(
+                AdditionalAnswers.returnsSecondArg());
 
         // Disable binder caches in this process.
         PropertyInvalidatedCache.disableForTestMode();
@@ -102,7 +110,7 @@
                     public void finishWrite(OutputStream os, boolean success) {}
                 }));
         mDeviceRepo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
     }
 
     @Test
@@ -111,7 +119,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
         assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight);
@@ -156,7 +164,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width not scaled
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
@@ -185,7 +193,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width re-scaled
         assertEquals(DISPLAY_WIDTH * 2, originalDisplayInfo.logicalWidth);
@@ -214,7 +222,7 @@
         mDisplayDeviceInfo.xDpi = 1.0f;
         mDisplayDeviceInfo.yDpi = 0.5f;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
@@ -234,7 +242,7 @@
         displayInfo.logicalHeight = DISPLAY_HEIGHT;
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         var updatedDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         assertEquals(Surface.ROTATION_90, updatedDisplayInfo.rotation);
@@ -277,7 +285,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
@@ -301,7 +309,7 @@
         mDisplayDeviceInfo.xDpi = 1.0f;
         mDisplayDeviceInfo.yDpi = 0.5f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width re-scaled
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
@@ -341,7 +349,7 @@
 
         expectedPosition.set(40, -20);
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         displayInfo.logicalWidth = DISPLAY_HEIGHT;
         displayInfo.logicalHeight = DISPLAY_WIDTH;
         displayInfo.rotation = Surface.ROTATION_90;
@@ -356,7 +364,7 @@
         mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
                 /*isAnisotropyCorrectionEnabled=*/ true,
                 /*isAlwaysRotateDisplayDeviceEnabled=*/ true);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         Point expectedPosition = new Point();
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
@@ -383,7 +391,7 @@
 
         expectedPosition.set(40, -20);
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         displayInfo.logicalWidth = DISPLAY_HEIGHT;
         displayInfo.logicalHeight = DISPLAY_WIDTH;
         displayInfo.rotation = Surface.ROTATION_90;
@@ -444,7 +452,7 @@
         // Update position and test to see that it's been updated to a rear, presentation display
         // that destroys content on removal
         mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertEquals(Display.FLAG_REAR | Display.FLAG_PRESENTATION,
                 mLogicalDisplay.getDisplayInfoLocked().flags);
         assertEquals(Display.REMOVE_MODE_DESTROY_CONTENT,
@@ -452,7 +460,7 @@
 
         // And then check the unsetting the position resets both
         mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_UNKNOWN);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
         assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
                 mLogicalDisplay.getDisplayInfoLocked().removeMode);
@@ -468,7 +476,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(layoutLimitedRefreshRate, info3.layoutLimitedRefreshRate);
@@ -483,7 +491,7 @@
         mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -497,7 +505,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertTrue(refreshRanges.contentEquals(info3.thermalRefreshRateThrottling));
@@ -512,7 +520,7 @@
         mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -525,7 +533,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(newId, info3.displayGroupId);
@@ -538,7 +546,7 @@
         mLogicalDisplay.updateDisplayGroupIdLocked(99);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -551,7 +559,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(brightnessThrottlingDataId, info3.thermalBrightnessThrottlingDataId);
@@ -564,7 +572,20 @@
         mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked("99");
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
+
+    @Test
+    public void testGetsAppSupportedModesFromSyntheticModeManager() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
+        Display.Mode[] appSupportedModes = new Display.Mode[] {new Display.Mode(OTHER_MODE_ID,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 45)};
+        when(mSyntheticModeManager.createAppSupportedModes(
+                any(), eq(mDisplayDeviceInfo.supportedModes))).thenReturn(appSupportedModes);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
+        DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked();
+        assertArrayEquals(appSupportedModes, info.appsSupportedModes);
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
index f0abcd2..cf6146f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
@@ -58,11 +58,14 @@
         val modes = arrayOf(
             Display.Mode(1, 1000, 1000, 60f),
             Display.Mode(2, 1000, 1000, 90f),
-            Display.Mode(3, 1000, 1000, 120f)
+            Display.Mode(3, 1000, 1000, 120f),
+            Display.Mode(99, 1000, 1000, 45f, 45f, true, floatArrayOf(), intArrayOf())
         )
-        displayModeDirector.injectSupportedModesByDisplay(SparseArray<Array<Display.Mode>>().apply {
-            append(Display.DEFAULT_DISPLAY, modes)
-        })
+
+        displayModeDirector.injectAppSupportedModesByDisplay(
+            SparseArray<Array<Display.Mode>>().apply {
+                append(Display.DEFAULT_DISPLAY, modes)
+            })
         displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply {
             append(Display.DEFAULT_DISPLAY, modes[0])
         })
@@ -116,7 +119,9 @@
             BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), null),
         PREFERRED_REFRESH_RATE_IGNORED(true, 0, 60f, 0f, 0f,
             null, null, null),
-        PREFERRED_REFRESH_RATE_INVALID(false, 0, 45f, 0f, 0f,
+        PREFERRED_REFRESH_RATE_INVALID(false, 0, 25f, 0f, 0f,
             null, null, null),
+        SYNTHETIC_MODE(false, 99, 0f, 0f, 0f,
+            RenderVote(45f, 45f), SizeVote(1000, 1000, 1000, 1000), null),
     }
 }
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt
new file mode 100644
index 0000000..5cd3a33
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode
+
+import android.view.Display.Mode
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.feature.DisplayManagerFlags
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+private val DISPLAY_MODES = arrayOf(
+    Mode(1, 100, 100, 60f),
+    Mode(2, 100, 100, 120f)
+)
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class SyntheticModeManagerTest {
+
+    private val mockFlags = mock<DisplayManagerFlags>()
+    private val mockConfig = mock<DisplayDeviceConfig>()
+
+    @Test
+    fun `test app supported modes`(@TestParameter testCase: AppSupportedModesTestCase) {
+        whenever(mockFlags.isSynthetic60HzModesEnabled).thenReturn(testCase.syntheticModesEnabled)
+        whenever(mockConfig.isVrrSupportEnabled).thenReturn(testCase.vrrSupported)
+        val syntheticModeManager = SyntheticModeManager(mockFlags)
+
+        val result = syntheticModeManager.createAppSupportedModes(
+            mockConfig, testCase.supportedModes)
+
+        assertThat(result).isEqualTo(testCase.expectedAppModes)
+    }
+
+    enum class AppSupportedModesTestCase(
+        val syntheticModesEnabled: Boolean,
+        val vrrSupported: Boolean,
+        val supportedModes: Array<Mode>,
+        val expectedAppModes: Array<Mode>
+    ) {
+        SYNTHETIC_MODES_NOT_SUPPORTED(false, true, DISPLAY_MODES, DISPLAY_MODES),
+        VRR_NOT_SUPPORTED(true, false, DISPLAY_MODES, DISPLAY_MODES),
+        VRR_SYNTHETIC_NOT_SUPPORTED(false, false, DISPLAY_MODES, DISPLAY_MODES),
+        SINGLE_RESOLUTION_MODES(true, true, DISPLAY_MODES, arrayOf(
+            Mode(2, 100, 100, 120f),
+            Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf())
+        )),
+        NO_60HZ_MODES(true, true, arrayOf(Mode(2, 100, 100, 120f)),
+            arrayOf(
+                Mode(2, 100, 100, 120f),
+                Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf())
+            )
+        ),
+        MULTI_RESOLUTION_MODES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 60f),
+                Mode(3, 300, 300, 60f),
+                Mode(4, 300, 300, 90f),
+                ),
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(4, 300, 300, 90f),
+                Mode(5, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+                Mode(6, 200, 200, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+                Mode(7, 300, 300, 60f, 60f, true, floatArrayOf(), intArrayOf())
+            )
+        ),
+        WITH_HDR_TYPES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f, 120f, false, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(2, 200, 200, 60f, 120f, false, floatArrayOf(), intArrayOf(3, 4)),
+                Mode(3, 200, 200, 120f, 120f, false, floatArrayOf(), intArrayOf(5, 6)),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 120f, 120f, false, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(3, 200, 200, 120f, 120f, false, floatArrayOf(), intArrayOf(5, 6)),
+                Mode(4, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(5, 200, 200, 60f, 60f, true, floatArrayOf(), intArrayOf(5, 6)),
+            )
+        ),
+        UNACHIEVABLE_60HZ(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 90f),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 90f),
+            )
+        ),
+        MULTI_RESOLUTION_MODES_WITH_UNACHIEVABLE_60HZ(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 90f),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 90f),
+                Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+            )
+        ),
+        LOWER_THAN_60HZ_MODES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 30f),
+                Mode(2, 100, 100, 45f),
+                Mode(3, 100, 100, 90f),
+            ),
+            arrayOf(
+                Mode(3, 100, 100, 90f),
+            )
+        ),
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
index 04b35f1..5da1bb6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
@@ -154,7 +154,7 @@
     }
 }
 private fun createMode(modeId: Int, refreshRate: Float, vsyncRate: Float): Display.Mode {
-    return Display.Mode(modeId, 600, 800, refreshRate, vsyncRate,
+    return Display.Mode(modeId, 600, 800, refreshRate, vsyncRate, false,
             FloatArray(0), IntArray(0))
 }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index c9a83b0..7ebf9ac 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -109,6 +109,7 @@
                         defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(),
                         LOW_REFRESH_RATE),
         };
+        mDisplayInfo.appsSupportedModes = mDisplayInfo.supportedModes;
         mDisplayInfo.defaultModeId = HI_MODE_ID;
         mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
     }