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