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.google.android.torus.core.power 18 19 /** 20 * This class determines ready-to-render conditions for the engine's main loop in order to target a 21 * requested frame rate. 22 */ 23 class FpsThrottler { 24 companion object { 25 private const val NANO_TO_MILLIS = 1 / 1E6 26 27 const val FPS_120 = 120f 28 const val FPS_60 = 60f 29 const val FPS_30 = 30f 30 const val FPS_18 = 18f 31 32 @Deprecated(message = "Use FPS_60 instead.") 33 const val HIGH_FPS = 60f 34 @Deprecated(message = "Use FPS_30 instead.") 35 const val MED_FPS = 30f 36 @Deprecated(message = "Use FPS_18 instead.") 37 const val LOW_FPS = 18f 38 } 39 40 private var fps: Float = FPS_60 41 42 @Volatile 43 private var frameTimeMillis: Double = 1000.0 / fps.toDouble() 44 private var lastFrameTimeNanos: Long = -1 45 46 @Volatile 47 private var continuousRenderingMode: Boolean = true 48 49 @Volatile 50 private var requestRendering: Boolean = false 51 updateFrameTimenull52 private fun updateFrameTime() { 53 frameTimeMillis = 1000.0 / fps.toDouble() 54 } 55 56 /** 57 * If [fps] is non-zero, update the requested FPS and calculate the frame time 58 * for the requested FPS. Otherwise disable continuous rendering (on demand rendering) 59 * without changing the frame rate. 60 * 61 * @param fps The requested FPS value. 62 */ updateFpsnull63 fun updateFps(fps: Float) { 64 if (fps <= 0f) { 65 setContinuousRenderingMode(false) 66 } else { 67 setContinuousRenderingMode(true) 68 this.fps = fps 69 updateFrameTime() 70 } 71 } 72 73 /** 74 * Sets rendering mode to continuous or on demand. 75 * 76 * @param continuousRenderingMode When true enable continuous rendering. When false disable 77 * continuous rendering (on demand). 78 */ setContinuousRenderingModenull79 fun setContinuousRenderingMode(continuousRenderingMode: Boolean) { 80 this.continuousRenderingMode = continuousRenderingMode 81 } 82 83 /** Request a new render frame (in on demand rendering mode). */ requestRenderingnull84 fun requestRendering() { 85 requestRendering = true 86 } 87 88 /** 89 * Calculates whether we can render the next frame. In continuous mode return true only 90 * if enough time has passed since the last render to maintain requested FPS. 91 * In on demand mode, return true only if [requestRendering] was called to render 92 * the next frame. 93 * 94 * @param frameTimeNanos The time in nanoseconds when the current frame started. 95 * 96 * @return true if we can render the next frame. 97 */ canRendernull98 fun canRender(frameTimeNanos: Long): Boolean { 99 return if (continuousRenderingMode) { 100 // continuous rendering 101 if (lastFrameTimeNanos == -1L) { 102 true 103 } else { 104 val deltaMillis = (frameTimeNanos - lastFrameTimeNanos) * NANO_TO_MILLIS 105 return (deltaMillis >= frameTimeMillis) && (fps > 0f) 106 } 107 } else { 108 // on demand rendering 109 requestRendering 110 } 111 } 112 113 /** 114 * Attempt to render a frame, if throttling permits it at this time. The delegate 115 * [onRenderPermitted] will be called to handle the rendering if so. The delegate may decide to 116 * skip the frame for any other reason, and then should return false. If the frame is actually 117 * rendered, the delegate must return true to ensure that the next frame will be scheduled for 118 * the correct time. 119 * 120 * @param frameTimeNanos The time in nanoseconds when the current frame started. 121 * @param onRenderPermitted The client delegate to dispatch if rendering is permitted at this 122 * time. 123 * 124 * @return true if a frame is permitted and then actually rendered. 125 */ tryRendernull126 fun tryRender(frameTimeNanos: Long, onRenderPermitted: () -> Boolean): Boolean { 127 if (canRender(frameTimeNanos) && onRenderPermitted()) { 128 // For pacing, record the time when the frame *started*, not when it finished rendering. 129 lastFrameTimeNanos = frameTimeNanos 130 requestRendering = false 131 return true 132 } 133 return false 134 } 135 } 136