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