/* * 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 #include #include #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(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( dlsym(mLibAndroid, "ANativeWindow_setFrameRate")); } mChoreographerFilter = std::make_unique( mCommonSettings.refreshPeriod, mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, [this](std::optional sfToVsyncDelay) { return wakeClient(sfToVsyncDelay); }); mChoreographerThread = ChoreographerThread::createChoreographerThread( ChoreographerThread::Type::Swappy, mJVM, jactivity, [this](std::optional sfToVsyncDelay) { mChoreographerFilter->onChoreographer(sfToVsyncDelay); }, [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(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( mCommonSettings.refreshPeriod, mCommonSettings.sfVsyncOffset - mCommonSettings.appVsyncOffset, [this](std::optional sfToVsyncDelay) { return wakeClient(sfToVsyncDelay); }); mUsingExternalChoreographer = true; mChoreographerThread = ChoreographerThread::createChoreographerThread( ChoreographerThread::Type::App, nullptr, nullptr, [this](std::optional sfToVsyncDelay) { mChoreographerFilter->onChoreographer(sfToVsyncDelay); }, [] {}, 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::optional sfToVsyncDelay) { std::lock_guard 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; mSfToVsyncDelay = sfToVsyncDelay; 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](std::optional sfToVsyncDelay) { mChoreographerFilter->onChoreographer(sfToVsyncDelay); }, [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 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(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(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 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 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 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(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 void addToTracers(Tracers& tracers, Func func, void* userData) { if (func != nullptr) { tracers.push_back({func, userData}); } } template 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 void executeTracers(T& tracers, Args... args) { for (const auto& tracer : tracers) { tracer.function(tracer.userData, std::forward(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 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(mPipelineMode)); } } void SwappyCommon::setAutoPipelineMode(bool enabled) { std::lock_guard lock(mMutex); mPipelineModeAutoMode = enabled; TRACE_INT("mPipelineModeAutoMode", mPipelineModeAutoMode); if (!enabled) { mPipelineMode = PipelineMode::On; TRACE_INT("mPipelineMode", static_cast(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) { const bool useSwappyDisplayManager = SwappyDisplayManager::useSwappyDisplayManager(mCommonSettings.sdkVersion); if (!useSwappyDisplayManager && 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 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 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::optional sfToVsyncDelay; { std::unique_lock lock(mWaitingMutex); currentFrame = mCurrentFrame; currentFrameTimestamp = mCurrentFrameTimestamp; sfToVsyncDelay = mSfToVsyncDelay; } // 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; if (sfToVsyncDelay) { expectedLatency += *sfToVsyncDelay / mCommonSettings.refreshPeriod; } 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; // If available, use the SF to Vsync delay to target the specific // vsync instead of guessing when the vsync is going to be if (sfToVsyncDelay) { currentFrameTimestamp += *sfToVsyncDelay - mCommonSettings.refreshPeriod / 2 - mMeasuredSwapDuration.load() - 1ms; } // 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 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 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 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 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 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 lock(mMutex); mBlockingWaitEnabled = enable; } } // namespace swappy