1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.display.mode;
18 
19 import android.annotation.Nullable;
20 import android.util.Slog;
21 import android.util.SparseArray;
22 import android.view.Display;
23 import android.view.SurfaceControl;
24 
25 import java.util.ArrayList;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Set;
29 
30 final class VoteSummary {
31     private static final float FLOAT_TOLERANCE = SurfaceControl.RefreshRateRange.FLOAT_TOLERANCE;
32     private static final String TAG = "VoteSummary";
33 
34     public float minPhysicalRefreshRate;
35     public float maxPhysicalRefreshRate;
36     public float minRenderFrameRate;
37     public float maxRenderFrameRate;
38     public int width;
39     public int height;
40     public int minWidth;
41     public int minHeight;
42     public boolean disableRefreshRateSwitching;
43     /**
44      *  available modes should have mode with specific refresh rate
45      */
46     public float appRequestBaseModeRefreshRate;
47     /**
48      * requestRefreshRate should be within [minRenderFrameRate, maxRenderFrameRate] range
49      */
50     public Set<Float> requestedRefreshRates = new HashSet<>();
51 
52     @Nullable
53     public List<SupportedRefreshRatesVote.RefreshRates> supportedRefreshRates;
54 
55     @Nullable
56     public List<Integer> supportedModeIds;
57 
58     final boolean mIsDisplayResolutionRangeVotingEnabled;
59 
60     private final boolean mSupportedModesVoteEnabled;
61     private final boolean mSupportsFrameRateOverride;
62     private final boolean mLoggingEnabled;
63 
VoteSummary(boolean isDisplayResolutionRangeVotingEnabled, boolean supportedModesVoteEnabled, boolean loggingEnabled, boolean supportsFrameRateOverride)64     VoteSummary(boolean isDisplayResolutionRangeVotingEnabled, boolean supportedModesVoteEnabled,
65             boolean loggingEnabled, boolean supportsFrameRateOverride) {
66         mIsDisplayResolutionRangeVotingEnabled = isDisplayResolutionRangeVotingEnabled;
67         mSupportedModesVoteEnabled = supportedModesVoteEnabled;
68         mLoggingEnabled = loggingEnabled;
69         mSupportsFrameRateOverride = supportsFrameRateOverride;
70         reset();
71     }
72 
applyVotes(SparseArray<Vote> votes, int lowestConsideredPriority, int highestConsideredPriority)73     void applyVotes(SparseArray<Vote> votes,
74             int lowestConsideredPriority, int highestConsideredPriority) {
75         reset();
76         for (int priority = highestConsideredPriority;
77                 priority >= lowestConsideredPriority;
78                 priority--) {
79             Vote vote = votes.get(priority);
80             if (vote == null) {
81                 continue;
82             }
83             vote.updateSummary(this);
84         }
85         if (mLoggingEnabled) {
86             Slog.i(TAG, "applyVotes for range ["
87                     + Vote.priorityToString(lowestConsideredPriority) + ", "
88                     + Vote.priorityToString(highestConsideredPriority) + "]: "
89                     + this);
90         }
91     }
92 
adjustSize(Display.Mode defaultMode, Display.Mode[] modes)93     void adjustSize(Display.Mode defaultMode, Display.Mode[] modes) {
94         // If we don't have anything specifying the width / height of the display, just use
95         // the default width and height. We don't want these switching out from underneath
96         // us since it's a pretty disruptive behavior.
97         if (height == Vote.INVALID_SIZE || width == Vote.INVALID_SIZE) {
98             width = defaultMode.getPhysicalWidth();
99             height = defaultMode.getPhysicalHeight();
100         } else if (mIsDisplayResolutionRangeVotingEnabled) {
101             updateSummaryWithBestAllowedResolution(modes);
102         }
103         if (mLoggingEnabled) {
104             Slog.i(TAG, "adjustSize: " + this);
105         }
106     }
107 
limitRefreshRanges(VoteSummary otherSummary)108     void limitRefreshRanges(VoteSummary otherSummary) {
109         minPhysicalRefreshRate =
110                 Math.min(minPhysicalRefreshRate, otherSummary.minPhysicalRefreshRate);
111         maxPhysicalRefreshRate =
112                 Math.max(maxPhysicalRefreshRate, otherSummary.maxPhysicalRefreshRate);
113         minRenderFrameRate = Math.min(minRenderFrameRate, otherSummary.minRenderFrameRate);
114         maxRenderFrameRate = Math.max(maxRenderFrameRate, otherSummary.maxRenderFrameRate);
115 
116         if (mLoggingEnabled) {
117             Slog.i(TAG, "limitRefreshRanges: " + this);
118         }
119     }
120 
filterModes(Display.Mode[] modes)121     List<Display.Mode> filterModes(Display.Mode[] modes) {
122         if (!isValid()) {
123             return new ArrayList<>();
124         }
125         ArrayList<Display.Mode> availableModes = new ArrayList<>();
126         boolean missingBaseModeRefreshRate = appRequestBaseModeRefreshRate > 0f;
127 
128         for (Display.Mode mode : modes) {
129             if (!validateRefreshRatesSupported(mode)) {
130                 continue;
131             }
132             if (!validateModeSupported(mode)) {
133                 continue;
134             }
135             if (!validateModeSize(mode)) {
136                 continue;
137             }
138             if (!validateModeWithinPhysicalRefreshRange(mode)) {
139                 continue;
140             }
141             if (!validateModeWithinRenderRefreshRange(mode)) {
142                 continue;
143             }
144             if (!validateModeRenderRateAchievable(mode)) {
145                 continue;
146             }
147             availableModes.add(mode);
148             if (equalsWithinFloatTolerance(mode.getRefreshRate(), appRequestBaseModeRefreshRate)) {
149                 missingBaseModeRefreshRate = false;
150             }
151         }
152         if (missingBaseModeRefreshRate) {
153             return new ArrayList<>();
154         }
155 
156         return availableModes;
157     }
158 
selectBaseMode(List<Display.Mode> availableModes, Display.Mode defaultMode)159     Display.Mode selectBaseMode(List<Display.Mode> availableModes, Display.Mode defaultMode) {
160         // The base mode should be as close as possible to the app requested mode. Since all the
161         // available modes already have the same size, we just need to look for a matching refresh
162         // rate. If the summary doesn't include an app requested refresh rate, we'll use the default
163         // mode refresh rate. This is important because SurfaceFlinger can do only seamless switches
164         // by default. Some devices (e.g. TV) don't support seamless switching so the mode we select
165         // here won't be changed.
166         float preferredRefreshRate =
167                 appRequestBaseModeRefreshRate > 0
168                         ? appRequestBaseModeRefreshRate : defaultMode.getRefreshRate();
169         for (Display.Mode availableMode : availableModes) {
170             if (equalsWithinFloatTolerance(preferredRefreshRate, availableMode.getRefreshRate())) {
171                 return availableMode;
172             }
173         }
174 
175         // If we couldn't find a mode id based on the refresh rate, it means that the available
176         // modes were filtered by the app requested size, which is different that the default mode
177         // size, and the requested app refresh rate was dropped from the summary due to a higher
178         // priority vote. Since we don't have any other hint about the refresh rate,
179         // we just pick the first.
180         return !availableModes.isEmpty() ? availableModes.get(0) : null;
181     }
182 
disableModeSwitching(float fps)183     void disableModeSwitching(float fps) {
184         minPhysicalRefreshRate = maxPhysicalRefreshRate = fps;
185         maxRenderFrameRate = Math.min(maxRenderFrameRate, fps);
186 
187         if (mLoggingEnabled) {
188             Slog.i(TAG, "Disabled mode switching on summary: " + this);
189         }
190     }
191 
disableRenderRateSwitching(float fps)192     void disableRenderRateSwitching(float fps) {
193         minRenderFrameRate = maxRenderFrameRate;
194 
195         if (!isRenderRateAchievable(fps)) {
196             minRenderFrameRate = maxRenderFrameRate = fps;
197         }
198 
199         if (mLoggingEnabled) {
200             Slog.i(TAG, "Disabled render rate switching on summary: " + this);
201         }
202     }
validateModeSize(Display.Mode mode)203     private boolean validateModeSize(Display.Mode mode) {
204         if (mode.getPhysicalWidth() != width
205                 || mode.getPhysicalHeight() != height) {
206             if (mLoggingEnabled) {
207                 Slog.w(TAG, "Discarding mode " + mode.getModeId() + ", wrong size"
208                         + ": desiredWidth=" + width
209                         + ": desiredHeight=" + height
210                         + ": actualWidth=" + mode.getPhysicalWidth()
211                         + ": actualHeight=" + mode.getPhysicalHeight());
212             }
213             return false;
214         }
215         return true;
216     }
217 
validateModeWithinPhysicalRefreshRange(Display.Mode mode)218     private boolean validateModeWithinPhysicalRefreshRange(Display.Mode mode) {
219         float refreshRate = mode.getRefreshRate();
220         // Some refresh rates are calculated based on frame timings, so they aren't *exactly*
221         // equal to expected refresh rate. Given that, we apply a bit of tolerance to this
222         // comparison.
223         if (refreshRate < (minPhysicalRefreshRate - FLOAT_TOLERANCE)
224                 || refreshRate > (maxPhysicalRefreshRate + FLOAT_TOLERANCE)) {
225             if (mLoggingEnabled) {
226                 Slog.w(TAG, "Discarding mode " + mode.getModeId()
227                         + ", outside refresh rate bounds"
228                         + ": minPhysicalRefreshRate=" + minPhysicalRefreshRate
229                         + ", maxPhysicalRefreshRate=" + maxPhysicalRefreshRate
230                         + ", modeRefreshRate=" + refreshRate);
231             }
232             return false;
233         }
234         return true;
235     }
236 
validateModeWithinRenderRefreshRange(Display.Mode mode)237     private boolean validateModeWithinRenderRefreshRange(Display.Mode mode) {
238         float refreshRate = mode.getRefreshRate();
239         // The physical refresh rate must be in the render frame rate range, unless
240         // frame rate override is supported.
241         if (!mSupportsFrameRateOverride) {
242             if (refreshRate < (minRenderFrameRate - FLOAT_TOLERANCE)
243                     || refreshRate > (maxRenderFrameRate + FLOAT_TOLERANCE)) {
244                 if (mLoggingEnabled) {
245                     Slog.w(TAG, "Discarding mode " + mode.getModeId()
246                             + ", outside render rate bounds"
247                             + ": minRenderFrameRate=" + minRenderFrameRate
248                             + ", maxRenderFrameRate=" + maxRenderFrameRate
249                             + ", modeRefreshRate=" + refreshRate);
250                 }
251                 return false;
252             }
253         }
254         return true;
255     }
256 
validateModeRenderRateAchievable(Display.Mode mode)257     private boolean validateModeRenderRateAchievable(Display.Mode mode) {
258         float refreshRate = mode.getRefreshRate();
259         if (!isRenderRateAchievable(refreshRate)) {
260             if (mLoggingEnabled) {
261                 Slog.w(TAG, "Discarding mode " + mode.getModeId()
262                         + ", outside frame rate bounds"
263                         + ": minRenderFrameRate=" + minRenderFrameRate
264                         + ", maxRenderFrameRate=" + maxRenderFrameRate
265                         + ", modePhysicalRefreshRate=" + refreshRate);
266             }
267             return false;
268         }
269         return true;
270     }
271 
validateModeSupported(Display.Mode mode)272     private boolean validateModeSupported(Display.Mode mode) {
273         if (supportedModeIds == null || !mSupportedModesVoteEnabled) {
274             return true;
275         }
276         if (supportedModeIds.contains(mode.getModeId())) {
277             return true;
278         }
279         if (mLoggingEnabled) {
280             Slog.w(TAG, "Discarding mode " + mode.getModeId()
281                     + ", supportedMode not found"
282                     + ": mode.modeId=" + mode.getModeId()
283                     + ", supportedModeIds=" + supportedModeIds);
284         }
285         return false;
286     }
287 
validateRefreshRatesSupported(Display.Mode mode)288     private boolean validateRefreshRatesSupported(Display.Mode mode) {
289         if (supportedRefreshRates == null || !mSupportedModesVoteEnabled) {
290             return true;
291         }
292         for (SupportedRefreshRatesVote.RefreshRates refreshRates : this.supportedRefreshRates) {
293             if (equalsWithinFloatTolerance(mode.getRefreshRate(), refreshRates.mPeakRefreshRate)
294                     && equalsWithinFloatTolerance(mode.getVsyncRate(), refreshRates.mVsyncRate)) {
295                 return true;
296             }
297         }
298         if (mLoggingEnabled) {
299             Slog.w(TAG, "Discarding mode " + mode.getModeId()
300                     + ", supportedRefreshRates not found"
301                     + ": mode.refreshRate=" + mode.getRefreshRate()
302                     + ", mode.vsyncRate=" + mode.getVsyncRate()
303                     + ", supportedRefreshRates=" + supportedRefreshRates);
304         }
305         return false;
306     }
307 
isRenderRateAchievable(float physicalRefreshRate)308     private boolean isRenderRateAchievable(float physicalRefreshRate) {
309         // Check whether the render frame rate range is achievable by the mode's physical
310         // refresh rate, meaning that if a divisor of the physical refresh rate is in range
311         // of the render frame rate.
312         // For example for the render frame rate [50, 70]:
313         //   - 120Hz is in range as we can render at 60hz by skipping every other frame,
314         //     which is within the render rate range
315         //   - 90hz is not in range as none of the even divisors (i.e. 90, 45, 30)
316         //     fall within the acceptable render range.
317         final int divisor =
318                 (int) Math.ceil((physicalRefreshRate / maxRenderFrameRate)
319                         - FLOAT_TOLERANCE);
320         float adjustedPhysicalRefreshRate = physicalRefreshRate / divisor;
321         return adjustedPhysicalRefreshRate >= (minRenderFrameRate - FLOAT_TOLERANCE);
322     }
323 
isValid()324     private boolean isValid() {
325         if (minRenderFrameRate > maxRenderFrameRate + FLOAT_TOLERANCE) {
326             if (mLoggingEnabled) {
327                 Slog.w(TAG, "Vote summary resulted in empty set (invalid frame rate range)"
328                         + ": minRenderFrameRate=" + minRenderFrameRate
329                         + ", maxRenderFrameRate=" + maxRenderFrameRate);
330             }
331             return false;
332         }
333 
334         if (supportedRefreshRates != null && mSupportedModesVoteEnabled
335                 && supportedRefreshRates.isEmpty()) {
336             if (mLoggingEnabled) {
337                 Slog.w(TAG, "Vote summary resulted in empty set (empty supportedModes)");
338             }
339             return false;
340         }
341 
342         for (Float requestedRefreshRate : requestedRefreshRates) {
343             if (requestedRefreshRate < minRenderFrameRate
344                     || requestedRefreshRate > maxRenderFrameRate) {
345                 if (mLoggingEnabled) {
346                     Slog.w(TAG, "Requested refreshRate is outside frame rate range"
347                             + ": requestedRefreshRates=" + requestedRefreshRates
348                             + ", requestedRefreshRate=" + requestedRefreshRate
349                             + ", minRenderFrameRate=" + minRenderFrameRate
350                             + ", maxRenderFrameRate=" + maxRenderFrameRate);
351                 }
352                 return false;
353             }
354         }
355 
356         return true;
357     }
358 
updateSummaryWithBestAllowedResolution(final Display.Mode[] supportedModes)359     private void updateSummaryWithBestAllowedResolution(final Display.Mode[] supportedModes) {
360         int maxAllowedWidth = width;
361         int maxAllowedHeight = height;
362         width = Vote.INVALID_SIZE;
363         height = Vote.INVALID_SIZE;
364         int maxNumberOfPixels = 0;
365         for (Display.Mode mode : supportedModes) {
366             if (mode.getPhysicalWidth() > maxAllowedWidth
367                     || mode.getPhysicalHeight() > maxAllowedHeight
368                     || mode.getPhysicalWidth() < minWidth
369                     || mode.getPhysicalHeight() < minHeight
370                     || mode.getRefreshRate() < (minPhysicalRefreshRate - FLOAT_TOLERANCE)
371                     || mode.getRefreshRate() > (maxPhysicalRefreshRate + FLOAT_TOLERANCE)
372             ) {
373                 continue;
374             }
375 
376             int numberOfPixels = mode.getPhysicalHeight() * mode.getPhysicalWidth();
377             if (numberOfPixels > maxNumberOfPixels || (mode.getPhysicalWidth() == maxAllowedWidth
378                     && mode.getPhysicalHeight() == maxAllowedHeight)) {
379                 maxNumberOfPixels = numberOfPixels;
380                 width = mode.getPhysicalWidth();
381                 height = mode.getPhysicalHeight();
382             }
383         }
384     }
385 
reset()386     private void reset() {
387         minPhysicalRefreshRate = 0f;
388         maxPhysicalRefreshRate = Float.POSITIVE_INFINITY;
389         minRenderFrameRate = 0f;
390         maxRenderFrameRate = Float.POSITIVE_INFINITY;
391         width = Vote.INVALID_SIZE;
392         height = Vote.INVALID_SIZE;
393         minWidth = 0;
394         minHeight = 0;
395         disableRefreshRateSwitching = false;
396         appRequestBaseModeRefreshRate = 0f;
397         requestedRefreshRates.clear();
398         supportedRefreshRates = null;
399         supportedModeIds = null;
400         if (mLoggingEnabled) {
401             Slog.i(TAG, "Summary reset: " + this);
402         }
403     }
404 
equalsWithinFloatTolerance(float a, float b)405     private static boolean equalsWithinFloatTolerance(float a, float b) {
406         return a >= b - FLOAT_TOLERANCE && a <= b + FLOAT_TOLERANCE;
407     }
408 
409     @Override
toString()410     public String toString() {
411         return  "VoteSummary{ minPhysicalRefreshRate=" + minPhysicalRefreshRate
412                 + ", maxPhysicalRefreshRate=" + maxPhysicalRefreshRate
413                 + ", minRenderFrameRate=" + minRenderFrameRate
414                 + ", maxRenderFrameRate=" + maxRenderFrameRate
415                 + ", width=" + width
416                 + ", height=" + height
417                 + ", minWidth=" + minWidth
418                 + ", minHeight=" + minHeight
419                 + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
420                 + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate
421                 + ", requestRefreshRates=" + requestedRefreshRates
422                 + ", supportedRefreshRates=" + supportedRefreshRates
423                 + ", supportedModeIds=" + supportedModeIds
424                 + ", mIsDisplayResolutionRangeVotingEnabled="
425                 + mIsDisplayResolutionRangeVotingEnabled
426                 + ", mSupportedModesVoteEnabled=" + mSupportedModesVoteEnabled
427                 + ", mSupportsFrameRateOverride=" + mSupportsFrameRateOverride + " }";
428     }
429 }
430