| /* |
| * Copyright 2018 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. |
| */ |
| |
| #include "SwappyCommon.h" |
| |
| #include <cmath> |
| #include <cstdlib> |
| #include <cstring> |
| |
| #include "Settings.h" |
| #include "Thread.h" |
| #include "Trace.h" |
| |
| #define LOG_TAG "SwappyCommon" |
| #include "SwappyLog.h" |
| |
| namespace swappy { |
| |
| using std::chrono::milliseconds; |
| using std::chrono::nanoseconds; |
| |
| // NB These are only needed for C++14 |
| constexpr nanoseconds SwappyCommon::FrameDuration::MAX_DURATION; |
| constexpr nanoseconds SwappyCommon::FRAME_MARGIN; |
| constexpr nanoseconds SwappyCommon::DURATION_ROUNDING_MARGIN; |
| constexpr nanoseconds SwappyCommon::REFRESH_RATE_MARGIN; |
| constexpr int SwappyCommon::NON_PIPELINE_PERCENT; |
| constexpr int SwappyCommon::FRAME_DROP_THRESHOLD; |
| constexpr std::chrono::nanoseconds |
| SwappyCommon::FrameDurations::FRAME_DURATION_SAMPLE_SECONDS; |
| |
| #if __ANDROID_API__ < 30 |
| // Define ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_* to allow compilation on older |
| // versions |
| enum { |
| /** |
| * There are no inherent restrictions on the frame rate of this window. |
| */ |
| ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT = 0, |
| /** |
| * This window is being used to display content with an inherently fixed |
| * frame rate, e.g. a video that has a specific frame rate. When the system |
| * selects a frame rate other than what the app requested, the app will need |
| * to do pull down or use some other technique to adapt to the system's |
| * frame rate. The user experience is likely to be worse (e.g. more frame |
| * stuttering) than it would be if the system had chosen the app's requested |
| * frame rate. |
| */ |
| ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1 |
| }; |
| #endif |
| |
| bool SwappyCommonSettings::getFromApp(JNIEnv* env, jobject jactivity, |
| SwappyCommonSettings* out) { |
| if (out == nullptr) return false; |
| |
| SWAPPY_LOGI("Swappy version %d.%d", SWAPPY_MAJOR_VERSION, |
| SWAPPY_MINOR_VERSION); |
| |
| out->sdkVersion = getSDKVersion(env); |
| |
| jclass activityClass = env->FindClass("android/app/NativeActivity"); |
| jclass windowManagerClass = env->FindClass("android/view/WindowManager"); |
| jclass displayClass = env->FindClass("android/view/Display"); |
| |
| jmethodID getWindowManager = env->GetMethodID( |
| activityClass, "getWindowManager", "()Landroid/view/WindowManager;"); |
| |
| jmethodID getDefaultDisplay = env->GetMethodID( |
| windowManagerClass, "getDefaultDisplay", "()Landroid/view/Display;"); |
| |
| jobject wm = env->CallObjectMethod(jactivity, getWindowManager); |
| jobject display = env->CallObjectMethod(wm, getDefaultDisplay); |
| |
| jmethodID getRefreshRate = |
| env->GetMethodID(displayClass, "getRefreshRate", "()F"); |
| |
| const float refreshRateHz = env->CallFloatMethod(display, getRefreshRate); |
| |
| jmethodID getAppVsyncOffsetNanos = |
| env->GetMethodID(displayClass, "getAppVsyncOffsetNanos", "()J"); |
| |
| // getAppVsyncOffsetNanos was only added in API 21. |
| // Return gracefully if this device doesn't support it. |
| if (getAppVsyncOffsetNanos == 0 || env->ExceptionOccurred()) { |
| SWAPPY_LOGE("Error while getting method: getAppVsyncOffsetNanos"); |
| env->ExceptionClear(); |
| return false; |
| } |
| const long appVsyncOffsetNanos = |
| env->CallLongMethod(display, getAppVsyncOffsetNanos); |
| |
| jmethodID getPresentationDeadlineNanos = |
| env->GetMethodID(displayClass, "getPresentationDeadlineNanos", "()J"); |
| |
| if (getPresentationDeadlineNanos == 0 || env->ExceptionOccurred()) { |
| SWAPPY_LOGE("Error while getting method: getPresentationDeadlineNanos"); |
| return false; |
| } |
| |
| const long vsyncPresentationDeadlineNanos = |
| env->CallLongMethod(display, getPresentationDeadlineNanos); |
| |
| const long ONE_MS_IN_NS = 1000 * 1000; |
| const long ONE_S_IN_NS = ONE_MS_IN_NS * 1000; |
| |
| const long vsyncPeriodNanos = |
| static_cast<long>(ONE_S_IN_NS / refreshRateHz); |
| const long sfVsyncOffsetNanos = |
| vsyncPeriodNanos - (vsyncPresentationDeadlineNanos - ONE_MS_IN_NS); |
| |
| using std::chrono::nanoseconds; |
| out->refreshPeriod = nanoseconds(vsyncPeriodNanos); |
| out->appVsyncOffset = nanoseconds(appVsyncOffsetNanos); |
| out->sfVsyncOffset = nanoseconds(sfVsyncOffsetNanos); |
| |
| return true; |
| } |
| |
| SwappyCommon::SwappyCommon(JNIEnv* env, jobject jactivity) |
| : mJactivity(env->NewGlobalRef(jactivity)), |
| mMeasuredSwapDuration(nanoseconds(0)), |
| mAutoSwapInterval(1), |
| mValid(false) { |
| mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); |
| if (mLibAndroid == nullptr) { |
| SWAPPY_LOGE("FATAL: cannot open libandroid.so: %s", strerror(errno)); |
| return; |
| } |
| |
| if (!SwappyCommonSettings::getFromApp(env, mJactivity, &mCommonSettings)) |
| return; |
| |
| env->GetJavaVM(&mJVM); |
| |
| if (isDeviceUnsupported()) { |
| SWAPPY_LOGE("Device is unsupported"); |
| return; |
| } |
| |
| if (!SwappyDisplayManager::useSwappyDisplayManager( |
| mCommonSettings.sdkVersion)) { |
| mANativeWindow_setFrameRate = |
| reinterpret_cast<PFN_ANativeWindow_setFrameRate>( |
| dlsym(mLibAndroid, "ANativeWindow_setFrameRate")); |
| } |
| |
| mChoreographerFilter = std::make_unique<ChoreographerFilter>( |
| mCommonSettings.refreshPeriod, |
| mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, |
| [this]() { return wakeClient(); }); |
| |
| mChoreographerThread = ChoreographerThread::createChoreographerThread( |
| ChoreographerThread::Type::Swappy, mJVM, jactivity, |
| [this] { mChoreographerFilter->onChoreographer(); }, |
| [this] { onRefreshRateChanged(); }, mCommonSettings.sdkVersion); |
| if (!mChoreographerThread->isInitialized()) { |
| SWAPPY_LOGE("failed to initialize ChoreographerThread"); |
| return; |
| } |
| if (USE_DISPLAY_MANAGER && |
| SwappyDisplayManager::usesMinSdkOrLater(mCommonSettings.sdkVersion)) { |
| mDisplayManager = |
| std::make_unique<SwappyDisplayManager>(mJVM, jactivity); |
| |
| if (!mDisplayManager->isInitialized()) { |
| mDisplayManager = nullptr; |
| SWAPPY_LOGE("failed to initialize DisplayManager"); |
| return; |
| } |
| } |
| |
| Settings::getInstance()->addListener([this]() { onSettingsChanged(); }); |
| Settings::getInstance()->setDisplayTimings({mCommonSettings.refreshPeriod, |
| mCommonSettings.appVsyncOffset, |
| mCommonSettings.sfVsyncOffset}); |
| |
| mInitialRefreshPeriod = mCommonSettings.refreshPeriod; |
| SWAPPY_LOGI( |
| "Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, " |
| "sfOffset=%lld", |
| (long long)mCommonSettings.refreshPeriod.count(), |
| (long long)mCommonSettings.appVsyncOffset.count(), |
| (long long)mCommonSettings.sfVsyncOffset.count()); |
| mValid = true; |
| } |
| |
| // Used by tests |
| SwappyCommon::SwappyCommon(const SwappyCommonSettings& settings) |
| : mJactivity(nullptr), |
| mCommonSettings(settings), |
| mMeasuredSwapDuration(nanoseconds(0)), |
| mAutoSwapInterval(1), |
| mValid(true) { |
| mChoreographerFilter = std::make_unique<ChoreographerFilter>( |
| mCommonSettings.refreshPeriod, |
| mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, |
| [this]() { return wakeClient(); }); |
| mUsingExternalChoreographer = true; |
| mChoreographerThread = ChoreographerThread::createChoreographerThread( |
| ChoreographerThread::Type::App, nullptr, nullptr, |
| [this] { mChoreographerFilter->onChoreographer(); }, [] {}, |
| mCommonSettings.sdkVersion); |
| |
| Settings::getInstance()->addListener([this]() { onSettingsChanged(); }); |
| Settings::getInstance()->setDisplayTimings({mCommonSettings.refreshPeriod, |
| mCommonSettings.appVsyncOffset, |
| mCommonSettings.sfVsyncOffset}); |
| |
| mInitialRefreshPeriod = mCommonSettings.refreshPeriod; |
| SWAPPY_LOGI( |
| "Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, " |
| "sfOffset=%lld", |
| (long long)mCommonSettings.refreshPeriod.count(), |
| (long long)mCommonSettings.appVsyncOffset.count(), |
| (long long)mCommonSettings.sfVsyncOffset.count()); |
| } |
| |
| SwappyCommon::~SwappyCommon() { |
| // Remove the settings' listeners before destroying Choreographer objects |
| // because the listeners may contain references to the objects |
| Settings::getInstance()->removeAllListeners(); |
| // destroy all threads first before the other members of this class |
| mChoreographerThread.reset(); |
| mChoreographerFilter.reset(); |
| |
| Settings::reset(); |
| |
| if (mJactivity != nullptr) { |
| JNIEnv* env; |
| mJVM->AttachCurrentThread(&env, nullptr); |
| |
| env->DeleteGlobalRef(mJactivity); |
| } |
| } |
| |
| void SwappyCommon::onRefreshRateChanged() { |
| JNIEnv* env; |
| mJVM->AttachCurrentThread(&env, nullptr); |
| |
| SWAPPY_LOGV("onRefreshRateChanged"); |
| |
| SwappyCommonSettings settings; |
| if (!SwappyCommonSettings::getFromApp(env, mJactivity, &settings)) { |
| SWAPPY_LOGE("failed to query display timings"); |
| return; |
| } |
| |
| Settings::getInstance()->setDisplayTimings({settings.refreshPeriod, |
| settings.appVsyncOffset, |
| settings.sfVsyncOffset}); |
| SWAPPY_LOGV("onRefreshRateChanged: refresh rate: %.0fHz", |
| 1e9f / settings.refreshPeriod.count()); |
| } |
| |
| nanoseconds SwappyCommon::wakeClient() { |
| std::lock_guard<std::mutex> lock(mWaitingMutex); |
| ++mCurrentFrame; |
| |
| // We're attempting to align with SurfaceFlinger's vsync, but it's always |
| // better to be a little late than a little early (since a little early |
| // could cause our frame to be picked up prematurely), so we pad by an |
| // additional millisecond. |
| mCurrentFrameTimestamp = |
| std::chrono::steady_clock::now() + mMeasuredSwapDuration.load() + 1ms; |
| mWaitingCondition.notify_all(); |
| return mMeasuredSwapDuration; |
| } |
| |
| void SwappyCommon::onChoreographer(int64_t frameTimeNanos) { |
| TRACE_CALL(); |
| |
| if (!mUsingExternalChoreographer) { |
| mUsingExternalChoreographer = true; |
| mChoreographerThread = ChoreographerThread::createChoreographerThread( |
| ChoreographerThread::Type::App, nullptr, nullptr, |
| [this] { mChoreographerFilter->onChoreographer(); }, |
| [this] { onRefreshRateChanged(); }, mCommonSettings.sdkVersion); |
| } |
| |
| mChoreographerThread->postFrameCallbacks(); |
| } |
| |
| bool SwappyCommon::waitForNextFrame(const SwapHandlers& h) { |
| int lateFrames = 0; |
| bool presentationTimeIsNeeded; |
| |
| // We do not want to hold the mutex while waiting, so make a local copy of |
| // the flags. |
| mMutex.lock(); |
| bool localAutoSwapIntervalEnabled = mAutoSwapIntervalEnabled; |
| bool localFramePacingEnabled = mFramePacingEnabled; |
| |
| // We do the blocking wait when pacing or request by the app when not |
| // pacing. |
| bool localBlockingWaitEnabled = mBlockingWaitEnabled || mFramePacingEnabled; |
| mMutex.unlock(); |
| |
| const nanoseconds cpuTime = |
| (mStartFrameTime.time_since_epoch().count() == 0) |
| ? 0ns |
| : std::chrono::steady_clock::now() - mStartFrameTime; |
| mCPUTracer.endTrace(); |
| |
| preWaitCallbacks(); |
| |
| // if we are running slower than the threshold (if auto swap interval is |
| // enabled) there is no point to sleep, |
| // just let the app run as fast as it can |
| if (mCommonSettings.refreshPeriod * mAutoSwapInterval <= |
| mAutoSwapIntervalThreshold.load() || |
| !localAutoSwapIntervalEnabled) { |
| if (localFramePacingEnabled) waitUntilTargetFrame(); |
| |
| if (localBlockingWaitEnabled) { |
| // wait for the previous frame to be rendered |
| while (!h.lastFrameIsComplete()) { |
| lateFrames++; |
| waitOneFrame(); |
| } |
| } |
| |
| mPresentationTime += lateFrames * mCommonSettings.refreshPeriod; |
| presentationTimeIsNeeded = localFramePacingEnabled; |
| } else { |
| presentationTimeIsNeeded = false; |
| } |
| |
| // If last frame is not finished, return -1 for GPU time. |
| const nanoseconds gpuTime = |
| (h.lastFrameIsComplete()) ? h.getPrevFrameGpuTime() : -1ns; |
| |
| // Keep track of durations only if frame pacing is enabled. |
| if (localFramePacingEnabled) |
| addFrameDuration({cpuTime, gpuTime, mCurrentFrame > mTargetFrame}); |
| |
| postWaitCallbacks(cpuTime, gpuTime); |
| |
| return presentationTimeIsNeeded; |
| } |
| |
| void SwappyCommon::updateDisplayTimings() { |
| // grab a pointer to the latest supported refresh rates |
| if (mDisplayManager) { |
| mSupportedRefreshPeriods = |
| mDisplayManager->getSupportedRefreshPeriods(); |
| } |
| |
| std::lock_guard<std::mutex> lock(mMutex); |
| SWAPPY_LOGW_ONCE_IF(!mWindow, |
| "ANativeWindow not configured, frame rate will not be " |
| "reported to Android platform"); |
| |
| if (mFramePacingToggleRequested) { |
| // In case frame pacing is toggled, set the update here. |
| mFramePacingEnabled = !mFramePacingEnabled; |
| mFramePacingToggleRequested = false; |
| } |
| if (mFramePacingResetRequested) { |
| // In case of reset, just issue the update for setting refresh period. |
| setPreferredRefreshPeriod(mInitialRefreshPeriod); |
| mFramePacingResetRequested = false; |
| return; |
| } |
| |
| if (!mFramePacingEnabled) { |
| return; |
| } |
| |
| if (!mTimingSettingsNeedUpdate && !mWindowChanged) { |
| return; |
| } |
| |
| mTimingSettingsNeedUpdate = false; |
| |
| if (!mWindowChanged && |
| mCommonSettings.refreshPeriod == mNextTimingSettings.refreshPeriod && |
| mSwapDuration == mNextTimingSettings.swapDuration) { |
| return; |
| } |
| |
| mWindowChanged = false; |
| mCommonSettings.refreshPeriod = mNextTimingSettings.refreshPeriod; |
| |
| const auto pipelineFrameTime = |
| mFrameDurations.getAverageFrameTime().getTime(PipelineMode::On); |
| const auto swapDuration = |
| pipelineFrameTime != 0ns ? pipelineFrameTime : mSwapDuration; |
| mAutoSwapInterval = |
| calculateSwapInterval(swapDuration, mCommonSettings.refreshPeriod); |
| mPipelineMode = PipelineMode::On; |
| |
| const bool swapIntervalValid = |
| mNextTimingSettings.refreshPeriod * mAutoSwapInterval >= |
| mNextTimingSettings.swapDuration; |
| const bool swapIntervalChangedBySettings = |
| mSwapDuration != mNextTimingSettings.swapDuration; |
| |
| mSwapDuration = mNextTimingSettings.swapDuration; |
| if (!mAutoSwapIntervalEnabled || swapIntervalChangedBySettings || |
| !swapIntervalValid) { |
| mAutoSwapInterval = |
| calculateSwapInterval(mSwapDuration, mCommonSettings.refreshPeriod); |
| mPipelineMode = PipelineMode::On; |
| setPreferredRefreshPeriod(mSwapDuration); |
| } |
| |
| if (mNextModeId == -1 && mLatestFrameRateVote == 0) { |
| setPreferredRefreshPeriod(mSwapDuration); |
| } |
| |
| mFrameDurations.clear(); |
| |
| TRACE_INT("mSwapDuration", int(mSwapDuration.count())); |
| TRACE_INT("mAutoSwapInterval", mAutoSwapInterval); |
| TRACE_INT("mCommonSettings.refreshPeriod", |
| mCommonSettings.refreshPeriod.count()); |
| TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode)); |
| } |
| |
| void SwappyCommon::onPreSwap(const SwapHandlers& h) { |
| if (!mUsingExternalChoreographer) { |
| mChoreographerThread->postFrameCallbacks(); |
| } |
| |
| // for non pipeline mode where both cpu and gpu work is done at the same |
| // stage wait for next frame will happen after swap |
| if (mPipelineMode == PipelineMode::On) { |
| mPresentationTimeNeeded = waitForNextFrame(h); |
| } else { |
| mPresentationTimeNeeded = |
| (mCommonSettings.refreshPeriod * mAutoSwapInterval <= |
| mAutoSwapIntervalThreshold.load()); |
| } |
| |
| mSwapTime = std::chrono::steady_clock::now(); |
| preSwapBuffersCallbacks(); |
| } |
| |
| void SwappyCommon::onPostSwap(const SwapHandlers& h) { |
| postSwapBuffersCallbacks(); |
| |
| updateMeasuredSwapDuration(std::chrono::steady_clock::now() - mSwapTime); |
| |
| if (mPipelineMode == PipelineMode::Off) { |
| waitForNextFrame(h); |
| } |
| |
| if (updateSwapInterval()) { |
| swapIntervalChangedCallbacks(); |
| TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode)); |
| TRACE_INT("mAutoSwapInterval", mAutoSwapInterval); |
| } |
| |
| updateDisplayTimings(); |
| |
| startFrame(); |
| } |
| |
| void SwappyCommon::updateMeasuredSwapDuration(nanoseconds duration) { |
| // TODO: The exponential smoothing factor here is arbitrary |
| mMeasuredSwapDuration = |
| (mMeasuredSwapDuration.load() * 4 / 5) + duration / 5; |
| |
| // Clamp the swap duration to half the refresh period |
| // |
| // We do this since the swap duration can be a bit noisy during periods such |
| // as app startup, which can cause some stuttering as the smoothing catches |
| // up with the actual duration. By clamping, we reduce the maximum error |
| // which reduces the calibration time. |
| if (mMeasuredSwapDuration.load() > (mCommonSettings.refreshPeriod / 2)) { |
| mMeasuredSwapDuration.store(mCommonSettings.refreshPeriod / 2); |
| } |
| } |
| |
| nanoseconds SwappyCommon::getSwapDuration() { |
| std::lock_guard<std::mutex> lock(mMutex); |
| return mAutoSwapInterval * mCommonSettings.refreshPeriod; |
| }; |
| |
| void SwappyCommon::FrameDurations::add(FrameDuration frameDuration) { |
| const auto now = std::chrono::steady_clock::now(); |
| mFrames.push_back({now, frameDuration}); |
| mFrameDurationsSum += frameDuration; |
| if (frameDuration.frameMiss()) { |
| mMissedFrameCount++; |
| } |
| |
| while (mFrames.size() >= 2 && |
| now - (mFrames.begin() + 1)->first > FRAME_DURATION_SAMPLE_SECONDS) { |
| mFrameDurationsSum -= mFrames.front().second; |
| if (mFrames.front().second.frameMiss()) { |
| mMissedFrameCount--; |
| } |
| mFrames.pop_front(); |
| } |
| } |
| |
| bool SwappyCommon::FrameDurations::hasEnoughSamples() const { |
| return (!mFrames.empty()) && (mFrames.back().first - mFrames.front().first > |
| FRAME_DURATION_SAMPLE_SECONDS); |
| } |
| |
| SwappyCommon::FrameDuration SwappyCommon::FrameDurations::getAverageFrameTime() |
| const { |
| if (hasEnoughSamples()) { |
| return mFrameDurationsSum / mFrames.size(); |
| } |
| |
| return {}; |
| } |
| |
| int SwappyCommon::FrameDurations::getMissedFramePercent() const { |
| return round(mMissedFrameCount * 100.0f / mFrames.size()); |
| } |
| |
| void SwappyCommon::FrameDurations::clear() { |
| mFrames.clear(); |
| mFrameDurationsSum = {}; |
| mMissedFrameCount = 0; |
| } |
| |
| void SwappyCommon::addFrameDuration(FrameDuration duration) { |
| SWAPPY_LOGV("cpuTime = %.2f", duration.getCpuTime().count() / 1e6f); |
| SWAPPY_LOGV("gpuTime = %.2f", duration.getGpuTime().count() / 1e6f); |
| SWAPPY_LOGV("frame %s", duration.frameMiss() ? "MISS" : "on time"); |
| |
| std::lock_guard<std::mutex> lock(mMutex); |
| mFrameDurations.add(duration); |
| } |
| |
| bool SwappyCommon::swapSlower(const FrameDuration& averageFrameTime, |
| const nanoseconds& upperBound, |
| int newSwapInterval) { |
| bool swappedSlower = false; |
| SWAPPY_LOGV("Rendering takes too much time for the given config"); |
| |
| const auto frameFitsUpperBound = |
| averageFrameTime.getTime(PipelineMode::On) <= upperBound; |
| const auto swapDurationWithinThreshold = |
| mCommonSettings.refreshPeriod * mAutoSwapInterval <= |
| mAutoSwapIntervalThreshold.load() + FRAME_MARGIN; |
| |
| // Check if turning on pipeline is not enough |
| if ((mPipelineMode == PipelineMode::On || !frameFitsUpperBound) && |
| swapDurationWithinThreshold) { |
| int originalAutoSwapInterval = mAutoSwapInterval; |
| if (newSwapInterval > mAutoSwapInterval) { |
| mAutoSwapInterval = newSwapInterval; |
| } else { |
| mAutoSwapInterval++; |
| } |
| if (mAutoSwapInterval != originalAutoSwapInterval) { |
| SWAPPY_LOGV("Changing Swap interval to %d from %d", |
| mAutoSwapInterval, originalAutoSwapInterval); |
| swappedSlower = true; |
| } |
| } |
| |
| if (mPipelineMode == PipelineMode::Off) { |
| SWAPPY_LOGV("turning on pipelining"); |
| mPipelineMode = PipelineMode::On; |
| } |
| |
| return swappedSlower; |
| } |
| |
| bool SwappyCommon::swapFaster(int newSwapInterval) { |
| bool swappedFaster = false; |
| int originalAutoSwapInterval = mAutoSwapInterval; |
| while (newSwapInterval < mAutoSwapInterval && swapFasterCondition()) { |
| mAutoSwapInterval--; |
| } |
| |
| if (mAutoSwapInterval != originalAutoSwapInterval) { |
| SWAPPY_LOGV("Rendering is much shorter for the given config"); |
| SWAPPY_LOGV("Changing Swap interval to %d from %d", mAutoSwapInterval, |
| originalAutoSwapInterval); |
| // since we changed the swap interval, we may need to turn on pipeline |
| // mode |
| SWAPPY_LOGV("Turning on pipelining"); |
| mPipelineMode = PipelineMode::On; |
| swappedFaster = true; |
| } |
| |
| return swappedFaster; |
| } |
| |
| bool SwappyCommon::updateSwapInterval() { |
| std::lock_guard<std::mutex> lock(mMutex); |
| |
| // A request to reset the frame-pacing is made, so reset the internal swap |
| // state to the initial state and clear the frame durations collected. |
| if (mFramePacingResetRequested) { |
| mAutoSwapInterval = 1; |
| mMeasuredSwapDuration = 0ns; |
| mSwapDuration = 0ns; |
| mCommonSettings.refreshPeriod = mInitialRefreshPeriod; |
| mFrameDurations.clear(); |
| return true; |
| } |
| if (!mAutoSwapIntervalEnabled) return false; |
| |
| if (!mFrameDurations.hasEnoughSamples()) return false; |
| |
| const auto averageFrameTime = mFrameDurations.getAverageFrameTime(); |
| const auto pipelineFrameTime = averageFrameTime.getTime(PipelineMode::On); |
| const auto nonPipelineFrameTime = |
| averageFrameTime.getTime(PipelineMode::Off); |
| |
| // calculate the new swap interval based on average frame time assume we are |
| // in pipeline mode (prefer higher swap interval rather than turning off |
| // pipeline mode) |
| const int newSwapInterval = |
| calculateSwapInterval(pipelineFrameTime, mCommonSettings.refreshPeriod); |
| |
| // Define upper and lower bounds based on the swap duration |
| const nanoseconds upperBoundForThisRefresh = |
| mCommonSettings.refreshPeriod * mAutoSwapInterval; |
| const nanoseconds lowerBoundForThisRefresh = |
| mCommonSettings.refreshPeriod * (mAutoSwapInterval - 1) - FRAME_MARGIN; |
| |
| const int missedFramesPercent = mFrameDurations.getMissedFramePercent(); |
| |
| SWAPPY_LOGV("mPipelineMode = %d", static_cast<int>(mPipelineMode)); |
| SWAPPY_LOGV("Average cpu frame time = %.2f", |
| (averageFrameTime.getCpuTime().count()) / 1e6f); |
| SWAPPY_LOGV("Average gpu frame time = %.2f", |
| (averageFrameTime.getGpuTime().count()) / 1e6f); |
| SWAPPY_LOGV("upperBound = %.2f", upperBoundForThisRefresh.count() / 1e6f); |
| SWAPPY_LOGV("lowerBound = %.2f", lowerBoundForThisRefresh.count() / 1e6f); |
| SWAPPY_LOGV("frame missed = %d%%", missedFramesPercent); |
| |
| bool configChanged = false; |
| SWAPPY_LOGV("pipelineFrameTime = %.2f", pipelineFrameTime.count() / 1e6f); |
| const auto nonPipelinePercent = (100.f + NON_PIPELINE_PERCENT) / 100.f; |
| |
| // Make sure the frame time fits in the current config to avoid missing |
| // frames |
| if (missedFramesPercent > FRAME_DROP_THRESHOLD) { |
| if (swapSlower(averageFrameTime, upperBoundForThisRefresh, |
| newSwapInterval)) |
| configChanged = true; |
| } |
| |
| // So we shouldn't miss any frames with this config but maybe we can go |
| // faster ? we check the pipeline frame time here as we prefer lower swap |
| // interval than no pipelining |
| else if (missedFramesPercent == 0 && swapFasterCondition() && |
| pipelineFrameTime < lowerBoundForThisRefresh) { |
| if (swapFaster(newSwapInterval)) configChanged = true; |
| } |
| |
| // If we reached to this condition it means that we fit into the boundaries. |
| // However we might be in pipeline mode and we could turn it off if we still |
| // fit. To be very conservative, switch to non-pipeline if frame time * 50% |
| // fits |
| else if (mPipelineModeAutoMode && mPipelineMode == PipelineMode::On && |
| nonPipelineFrameTime * nonPipelinePercent < |
| upperBoundForThisRefresh) { |
| SWAPPY_LOGV( |
| "Rendering time fits the current swap interval without pipelining"); |
| mPipelineMode = PipelineMode::Off; |
| configChanged = true; |
| } |
| |
| if (configChanged) { |
| mFrameDurations.clear(); |
| } |
| |
| setPreferredRefreshPeriod(pipelineFrameTime); |
| |
| return configChanged; |
| } |
| |
| template <typename Tracers, typename Func> |
| void addToTracers(Tracers& tracers, Func func, void* userData) { |
| if (func != nullptr) { |
| tracers.push_back({func, userData}); |
| } |
| } |
| |
| template <typename Tracers, typename Func> |
| void removeFromTracers(Tracers& tracers, Func func) { |
| if (func != nullptr) { |
| for (auto it = tracers.begin(); it != tracers.end();) { |
| auto jt = it; |
| it++; |
| if (jt->function == func) { |
| tracers.erase(jt); |
| } |
| } |
| } |
| } |
| |
| void SwappyCommon::addTracerCallbacks(const SwappyTracer& tracer) { |
| addToTracers(mInjectedTracers.preWait, tracer.preWait, tracer.userData); |
| addToTracers(mInjectedTracers.postWait, tracer.postWait, tracer.userData); |
| addToTracers(mInjectedTracers.preSwapBuffers, tracer.preSwapBuffers, |
| tracer.userData); |
| addToTracers(mInjectedTracers.postSwapBuffers, tracer.postSwapBuffers, |
| tracer.userData); |
| addToTracers(mInjectedTracers.startFrame, tracer.startFrame, |
| tracer.userData); |
| addToTracers(mInjectedTracers.swapIntervalChanged, |
| tracer.swapIntervalChanged, tracer.userData); |
| } |
| |
| void SwappyCommon::removeTracerCallbacks(const SwappyTracer& tracer) { |
| removeFromTracers(mInjectedTracers.preWait, tracer.preWait); |
| removeFromTracers(mInjectedTracers.postWait, tracer.postWait); |
| removeFromTracers(mInjectedTracers.preSwapBuffers, tracer.preSwapBuffers); |
| removeFromTracers(mInjectedTracers.postSwapBuffers, tracer.postSwapBuffers); |
| removeFromTracers(mInjectedTracers.startFrame, tracer.startFrame); |
| removeFromTracers(mInjectedTracers.swapIntervalChanged, |
| tracer.swapIntervalChanged); |
| } |
| |
| template <typename T, typename... Args> |
| void executeTracers(T& tracers, Args... args) { |
| for (const auto& tracer : tracers) { |
| tracer.function(tracer.userData, std::forward<Args>(args)...); |
| } |
| } |
| |
| void SwappyCommon::preSwapBuffersCallbacks() { |
| executeTracers(mInjectedTracers.preSwapBuffers); |
| } |
| |
| void SwappyCommon::postSwapBuffersCallbacks() { |
| executeTracers(mInjectedTracers.postSwapBuffers, |
| (int64_t)mPresentationTime.time_since_epoch().count()); |
| } |
| |
| void SwappyCommon::preWaitCallbacks() { |
| executeTracers(mInjectedTracers.preWait); |
| } |
| |
| void SwappyCommon::postWaitCallbacks(nanoseconds cpuTime, nanoseconds gpuTime) { |
| executeTracers(mInjectedTracers.postWait, cpuTime.count(), gpuTime.count()); |
| } |
| |
| void SwappyCommon::startFrameCallbacks() { |
| executeTracers(mInjectedTracers.startFrame, mCurrentFrame, |
| (int64_t)mPresentationTime.time_since_epoch().count()); |
| } |
| |
| void SwappyCommon::swapIntervalChangedCallbacks() { |
| executeTracers(mInjectedTracers.swapIntervalChanged); |
| } |
| |
| void SwappyCommon::setAutoSwapInterval(bool enabled) { |
| std::lock_guard<std::mutex> lock(mMutex); |
| mAutoSwapIntervalEnabled = enabled; |
| |
| // non pipeline mode is not supported when auto mode is disabled |
| if (!enabled) { |
| mPipelineMode = PipelineMode::On; |
| TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode)); |
| } |
| } |
| |
| void SwappyCommon::setAutoPipelineMode(bool enabled) { |
| std::lock_guard<std::mutex> lock(mMutex); |
| mPipelineModeAutoMode = enabled; |
| TRACE_INT("mPipelineModeAutoMode", mPipelineModeAutoMode); |
| if (!enabled) { |
| mPipelineMode = PipelineMode::On; |
| TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode)); |
| } |
| } |
| |
| void SwappyCommon::setPreferredDisplayModeId(int modeId) { |
| if (!mDisplayManager || modeId < 0 || mNextModeId == modeId) { |
| return; |
| } |
| |
| mNextModeId = modeId; |
| mDisplayManager->setPreferredDisplayModeId(modeId); |
| SWAPPY_LOGV("setPreferredDisplayModeId set to %d", modeId); |
| } |
| |
| int SwappyCommon::calculateSwapInterval(nanoseconds frameTime, |
| nanoseconds refreshPeriod) { |
| if (frameTime < refreshPeriod) { |
| return 1; |
| } |
| |
| auto div_result = div(frameTime.count(), refreshPeriod.count()); |
| auto framesPerRefresh = div_result.quot; |
| auto framesPerRefreshRemainder = div_result.rem; |
| |
| return (framesPerRefresh + |
| (framesPerRefreshRemainder > REFRESH_RATE_MARGIN.count() ? 1 : 0)); |
| } |
| |
| void SwappyCommon::setPreferredRefreshPeriod(nanoseconds frameTime) { |
| if (mANativeWindow_setFrameRate && mWindow) { |
| auto frameRate = 1e9f / frameTime.count(); |
| |
| frameRate = std::min(frameRate, 1e9f / (mSwapDuration).count()); |
| if (std::abs(mLatestFrameRateVote - frameRate) > |
| FRAME_RATE_VOTE_MARGIN) { |
| mLatestFrameRateVote = frameRate; |
| SWAPPY_LOGV("ANativeWindow_setFrameRate(%.2f)", frameRate); |
| mANativeWindow_setFrameRate( |
| mWindow, frameRate, |
| ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT); |
| } |
| |
| TRACE_INT("preferredRefreshPeriod", (int)frameRate); |
| } else { |
| if (!mDisplayManager || !mSupportedRefreshPeriods) { |
| return; |
| } |
| // Loop across all supported refresh periods to find the best refresh |
| // period. Best refresh period means: |
| // Shortest swap period that can still accommodate the frame time |
| // and that has the longest refresh period possible to optimize |
| // power consumption. |
| std::pair<nanoseconds, int> bestRefreshConfig; |
| nanoseconds minSwapDuration = 1s; |
| for (const auto& refreshConfig : *mSupportedRefreshPeriods) { |
| const auto period = refreshConfig.first; |
| const int swapIntervalForPeriod = |
| calculateSwapInterval(frameTime, period); |
| const nanoseconds swapDuration = period * swapIntervalForPeriod; |
| |
| // Don't allow swapping faster than mSwapDuration (see public |
| // header) |
| if (swapDuration + FRAME_MARGIN < mSwapDuration) { |
| continue; |
| } |
| |
| // We iterate in ascending order of refresh period, so accepting any |
| // better or equal-within-margin duration here chooses the longest |
| // refresh period possible. |
| if (swapDuration < minSwapDuration + FRAME_MARGIN) { |
| minSwapDuration = swapDuration; |
| bestRefreshConfig = refreshConfig; |
| } |
| } |
| |
| // Switch if we have a potentially better refresh rate |
| { |
| TRACE_INT("preferredRefreshPeriod", |
| bestRefreshConfig.first.count()); |
| setPreferredDisplayModeId(bestRefreshConfig.second); |
| } |
| } |
| } |
| |
| void SwappyCommon::onSettingsChanged() { |
| std::lock_guard<std::mutex> lock(mMutex); |
| |
| TimingSettings timingSettings = |
| TimingSettings::from(*Settings::getInstance()); |
| |
| // If display timings has changed, cache the update and apply them on the |
| // next frame |
| if (timingSettings != mNextTimingSettings) { |
| mNextTimingSettings = timingSettings; |
| mTimingSettingsNeedUpdate = true; |
| } |
| } |
| |
| void SwappyCommon::startFrame() { |
| TRACE_CALL(); |
| |
| int32_t currentFrame; |
| std::chrono::steady_clock::time_point currentFrameTimestamp; |
| { |
| std::unique_lock<std::mutex> lock(mWaitingMutex); |
| currentFrame = mCurrentFrame; |
| currentFrameTimestamp = mCurrentFrameTimestamp; |
| } |
| |
| // Whether to add a wait to fix buffer stuffing. |
| bool waitFrame = false; |
| |
| const int intervals = (mPipelineMode == PipelineMode::On) ? 2 : 1; |
| |
| // Use frame statistics to fix any buffer stuffing |
| if (mBufferStuffingFixWait > 0 && mLastLatencyRecorded) { |
| int32_t lastLatency = mLastLatencyRecorded(); |
| int expectedLatency = mAutoSwapInterval * intervals; |
| TRACE_INT("ExpectedLatency", expectedLatency); |
| if (mBufferStuffingFixCounter == 0) { |
| if (lastLatency > expectedLatency) { |
| mMissedFrameCounter++; |
| if (mMissedFrameCounter >= mBufferStuffingFixWait) { |
| waitFrame = true; |
| mBufferStuffingFixCounter = 2 * lastLatency; |
| TRACE_INT("BufferStuffingFix", mBufferStuffingFixCounter); |
| } |
| } else { |
| mMissedFrameCounter = 0; |
| } |
| } else { |
| --mBufferStuffingFixCounter; |
| TRACE_INT("BufferStuffingFix", mBufferStuffingFixCounter); |
| } |
| } |
| mTargetFrame = currentFrame + mAutoSwapInterval; |
| if (waitFrame) mTargetFrame += 1; |
| |
| // We compute the target time as now |
| // + the time the buffer will be on the GPU and in the queue to the |
| // compositor (1 swap period) |
| mPresentationTime = |
| currentFrameTimestamp + |
| (mAutoSwapInterval * intervals) * mCommonSettings.refreshPeriod; |
| |
| mStartFrameTime = std::chrono::steady_clock::now(); |
| mCPUTracer.startTrace(); |
| |
| startFrameCallbacks(); |
| } |
| |
| void SwappyCommon::waitUntil(int32_t target) { |
| TRACE_CALL(); |
| std::unique_lock<std::mutex> lock(mWaitingMutex); |
| mWaitingCondition.wait(lock, [&]() { |
| if (mCurrentFrame < target) { |
| if (!mUsingExternalChoreographer) { |
| mChoreographerThread->postFrameCallbacks(); |
| } |
| return false; |
| } |
| return true; |
| }); |
| } |
| |
| void SwappyCommon::waitUntilTargetFrame() { waitUntil(mTargetFrame); } |
| |
| void SwappyCommon::waitOneFrame() { waitUntil(mCurrentFrame + 1); } |
| |
| SdkVersion SwappyCommonSettings::getSDKVersion(JNIEnv* env) { |
| const jclass buildClass = env->FindClass("android/os/Build$VERSION"); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get Build.VERSION class"); |
| return SdkVersion{0, 0}; |
| } |
| |
| const jfieldID sdkInt = env->GetStaticFieldID(buildClass, "SDK_INT", "I"); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get Build.VERSION.SDK_INT field"); |
| return SdkVersion{0, 0}; |
| } |
| |
| const jint sdk = env->GetStaticIntField(buildClass, sdkInt); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get SDK version"); |
| return SdkVersion{0, 0}; |
| } |
| |
| jint sdkPreview = 0; |
| if (sdk >= 23) { |
| const jfieldID previewSdkInt = |
| env->GetStaticFieldID(buildClass, "PREVIEW_SDK_INT", "I"); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get Build.VERSION.PREVIEW_SDK_INT field"); |
| } |
| |
| sdkPreview = env->GetStaticIntField(buildClass, previewSdkInt); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get preview SDK version"); |
| } |
| } |
| |
| SWAPPY_LOGI("SDK version = %d preview = %d", sdk, sdkPreview); |
| return SdkVersion{sdk, sdkPreview}; |
| } |
| |
| void SwappyCommon::setANativeWindow(ANativeWindow* window) { |
| std::lock_guard<std::mutex> lock(mMutex); |
| if (mWindow == window) { |
| return; |
| } |
| |
| if (mWindow != nullptr) { |
| ANativeWindow_release(mWindow); |
| } |
| |
| mWindow = window; |
| if (mWindow != nullptr) { |
| ANativeWindow_acquire(mWindow); |
| mWindowChanged = true; |
| mLatestFrameRateVote = 0; |
| } |
| } |
| |
| namespace { |
| |
| static std::string GetStaticStringField(JNIEnv* env, jclass clz, |
| const char* name) { |
| const jfieldID fieldId = |
| env->GetStaticFieldID(clz, name, "Ljava/lang/String;"); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get string field %s", name); |
| return ""; |
| } |
| |
| const jstring jstr = (jstring)env->GetStaticObjectField(clz, fieldId); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get string %s", name); |
| return ""; |
| } |
| auto cstr = env->GetStringUTFChars(jstr, nullptr); |
| auto length = env->GetStringUTFLength(jstr); |
| std::string retValue(cstr, length); |
| env->ReleaseStringUTFChars(jstr, cstr); |
| env->DeleteLocalRef(jstr); |
| return retValue; |
| } |
| |
| struct DeviceIdentifier { |
| std::string manufacturer; |
| std::string model; |
| std::string display; |
| // Empty fields match against any value and we match the beginning of the |
| // input, e.g. |
| // A37 matches A37f, A37fw, etc. |
| bool match(const std::string& manufacturer_in, const std::string& model_in, |
| const std::string& display_in) { |
| if (!matchStartOfString(manufacturer, manufacturer_in)) return false; |
| if (!matchStartOfString(model, model_in)) return false; |
| if (!matchStartOfString(display, display_in)) return false; |
| return true; |
| } |
| bool matchStartOfString(const std::string& start, |
| const std::string& sample) { |
| return start.empty() || start == sample.substr(0, start.length()); |
| } |
| }; |
| |
| } // anonymous namespace |
| |
| bool SwappyCommon::isDeviceUnsupported() { |
| JNIEnv* env; |
| mJVM->AttachCurrentThread(&env, nullptr); |
| |
| // List of unsupported models |
| static std::vector<DeviceIdentifier> unsupportedDevices = { |
| {"OPPO", "A37", ""}}; |
| |
| const jclass buildClass = env->FindClass("android/os/Build"); |
| if (env->ExceptionCheck()) { |
| env->ExceptionClear(); |
| SWAPPY_LOGE("Failed to get Build class"); |
| return false; |
| } |
| |
| auto manufacturer = GetStaticStringField(env, buildClass, "MANUFACTURER"); |
| if (manufacturer.empty()) return false; |
| |
| auto model = GetStaticStringField(env, buildClass, "MODEL"); |
| if (model.empty()) return false; |
| |
| auto display = GetStaticStringField(env, buildClass, "DISPLAY"); |
| if (display.empty()) return false; |
| |
| for (auto& device : unsupportedDevices) { |
| if (device.match(manufacturer, model, display)) return true; |
| } |
| |
| return false; |
| } |
| |
| int SwappyCommon::getSupportedRefreshPeriodsNS(uint64_t* out_refreshrates, |
| int allocated_entries) { |
| if (mDisplayManager) { |
| mSupportedRefreshPeriods = |
| mDisplayManager->getSupportedRefreshPeriods(); |
| } |
| |
| if (!mSupportedRefreshPeriods) return 0; |
| if (!out_refreshrates) return (*mSupportedRefreshPeriods).size(); |
| |
| int counter = 0; |
| for (const auto& pair : *mSupportedRefreshPeriods) { |
| out_refreshrates[counter] = pair.first.count(); |
| ++counter; |
| } |
| |
| return (*mSupportedRefreshPeriods).size(); |
| } |
| |
| void SwappyCommon::resetFramePacing() { |
| std::lock_guard<std::mutex> lock(mMutex); |
| |
| // Just set the flag here, we actually reset at the end of the frame. |
| mFramePacingResetRequested = true; |
| } |
| |
| void SwappyCommon::enableFramePacing(bool enable) { |
| std::lock_guard<std::mutex> lock(mMutex); |
| |
| // Set flags that are applied at the end of the frame. |
| if (mFramePacingEnabled != enable) { |
| mFramePacingToggleRequested = true; |
| mFramePacingResetRequested = true; |
| } |
| } |
| |
| void SwappyCommon::enableBlockingWait(bool enable) { |
| std::lock_guard<std::mutex> lock(mMutex); |
| |
| mBlockingWaitEnabled = enable; |
| } |
| |
| } // namespace swappy |