/* * Copyright 2021 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. */ #undef LOG_TAG #define LOG_TAG "Planner" // #define LOG_NDEBUG 0 #define ATRACE_TAG ATRACE_TAG_GRAPHICS #include #include #include #include using time_point = std::chrono::steady_clock::time_point; using namespace std::chrono_literals; namespace android::compositionengine::impl::planner { namespace { // True if the underlying layer stack is the same modulo state that would be expected to be // different like specific buffers, false otherwise. bool isSameStack(const std::vector& incomingLayers, const std::vector& cachedSets) { std::vector existingLayers; for (auto& cachedSet : cachedSets) { for (auto& layer : cachedSet.getConstituentLayers()) { existingLayers.push_back(layer.getState()); } } if (incomingLayers.size() != existingLayers.size()) { return false; } for (size_t i = 0; i < incomingLayers.size(); i++) { // Checking the IDs here is very strict, but we do this as otherwise we may mistakenly try // to access destroyed OutputLayers later on. if (incomingLayers[i]->getId() != existingLayers[i]->getId() || incomingLayers[i]->getDifferingFields(*(existingLayers[i])) != LayerStateField::None) { return false; } } return true; } } // namespace Flattener::Flattener( renderengine::RenderEngine& renderEngine, bool enableHolePunch, std::optional cachedSetRenderSchedulingTunables) : mRenderEngine(renderEngine), mEnableHolePunch(enableHolePunch), mCachedSetRenderSchedulingTunables(cachedSetRenderSchedulingTunables), mTexturePool(mRenderEngine) { const int timeoutInMs = base::GetIntProperty(std::string("debug.sf.layer_caching_active_layer_timeout_ms"), 0); if (timeoutInMs != 0) { mActiveLayerTimeout = std::chrono::milliseconds(timeoutInMs); } } NonBufferHash Flattener::flattenLayers(const std::vector& layers, NonBufferHash hash, time_point now) { ATRACE_CALL(); const size_t unflattenedDisplayCost = calculateDisplayCost(layers); mUnflattenedDisplayCost += unflattenedDisplayCost; // We invalidate the layer cache if: // 1. We're not tracking any layers, or // 2. The last seen hashed geometry changed between frames, or // 3. A stricter equality check demonstrates that the layer stack really did change, since the // hashed geometry does not guarantee uniqueness. if (mCurrentGeometry != hash || (!mLayers.empty() && !isSameStack(layers, mLayers))) { resetActivities(hash, now); mFlattenedDisplayCost += unflattenedDisplayCost; return hash; } ++mInitialLayerCounts[layers.size()]; // Only buildCachedSets if these layers are already stored in mLayers. // Otherwise (i.e. mergeWithCachedSets returns false), the time has not // changed, so buildCachedSets will never find any runs. const bool alreadyHadCachedSets = mergeWithCachedSets(layers, now); ++mFinalLayerCounts[mLayers.size()]; if (alreadyHadCachedSets) { buildCachedSets(now); hash = computeLayersHash(); } return hash; } void Flattener::renderCachedSets( const OutputCompositionState& outputState, std::optional renderDeadline) { ATRACE_CALL(); if (!mNewCachedSet) { return; } // Ensure that a cached set has a valid buffer first if (mNewCachedSet->hasRenderedBuffer()) { ATRACE_NAME("mNewCachedSet->hasRenderedBuffer()"); return; } const auto now = std::chrono::steady_clock::now(); // If we have a render deadline, and the flattener is configured to skip rendering if we don't // have enough time, then we skip rendering the cached set if we think that we'll steal too much // time from the next frame. if (renderDeadline && mCachedSetRenderSchedulingTunables) { if (const auto estimatedRenderFinish = now + mCachedSetRenderSchedulingTunables->cachedSetRenderDuration; estimatedRenderFinish > *renderDeadline) { mNewCachedSet->incrementSkipCount(); if (mNewCachedSet->getSkipCount() <= mCachedSetRenderSchedulingTunables->maxDeferRenderAttempts) { ATRACE_FORMAT("DeadlinePassed: exceeded deadline by: %d us", std::chrono::duration_cast( estimatedRenderFinish - *renderDeadline) .count()); return; } else { ATRACE_NAME("DeadlinePassed: exceeded max skips"); } } } mNewCachedSet->render(mRenderEngine, mTexturePool, outputState); } void Flattener::dumpLayers(std::string& result) const { result.append(" Current layers:"); for (const CachedSet& layer : mLayers) { result.append("\n"); layer.dump(result); } } void Flattener::dump(std::string& result) const { const auto now = std::chrono::steady_clock::now(); base::StringAppendF(&result, "Flattener state:\n"); result.append("\n Statistics:\n"); result.append(" Display cost (in screen-size buffers):\n"); const size_t displayArea = static_cast(mDisplaySize.width * mDisplaySize.height); base::StringAppendF(&result, " Unflattened: %.2f\n", static_cast(mUnflattenedDisplayCost) / displayArea); base::StringAppendF(&result, " Flattened: %.2f\n", static_cast(mFlattenedDisplayCost) / displayArea); const auto compareLayerCounts = [](const std::pair& left, const std::pair& right) { return left.first < right.first; }; const size_t maxLayerCount = mInitialLayerCounts.empty() ? 0u : std::max_element(mInitialLayerCounts.cbegin(), mInitialLayerCounts.cend(), compareLayerCounts) ->first; result.append("\n Initial counts:\n"); for (size_t count = 1; count < maxLayerCount; ++count) { size_t initial = mInitialLayerCounts.count(count) > 0 ? mInitialLayerCounts.at(count) : 0; base::StringAppendF(&result, " % 2zd: %zd\n", count, initial); } result.append("\n Final counts:\n"); for (size_t count = 1; count < maxLayerCount; ++count) { size_t final = mFinalLayerCounts.count(count) > 0 ? mFinalLayerCounts.at(count) : 0; base::StringAppendF(&result, " % 2zd: %zd\n", count, final); } base::StringAppendF(&result, "\n Cached sets created: %zd\n", mCachedSetCreationCount); base::StringAppendF(&result, " Cost: %.2f\n", static_cast(mCachedSetCreationCost) / displayArea); const auto lastUpdate = std::chrono::duration_cast(now - mLastGeometryUpdate); base::StringAppendF(&result, "\n Current hash %016zx, last update %sago\n\n", mCurrentGeometry, durationString(lastUpdate).c_str()); dumpLayers(result); } size_t Flattener::calculateDisplayCost(const std::vector& layers) const { Region coveredRegion; size_t displayCost = 0; bool hasClientComposition = false; for (const LayerState* layer : layers) { coveredRegion.orSelf(layer->getDisplayFrame()); // Regardless of composition type, we always have to read each input once displayCost += static_cast(layer->getDisplayFrame().width() * layer->getDisplayFrame().height()); hasClientComposition |= layer->getCompositionType() == hal::Composition::CLIENT; } if (hasClientComposition) { // If there is client composition, the client target buffer has to be both written by the // GPU and read by the DPU, so we pay its cost twice displayCost += 2 * static_cast(coveredRegion.bounds().width() * coveredRegion.bounds().height()); } return displayCost; } void Flattener::resetActivities(NonBufferHash hash, time_point now) { ALOGV("[%s]", __func__); mCurrentGeometry = hash; mLastGeometryUpdate = now; for (const CachedSet& cachedSet : mLayers) { if (cachedSet.getLayerCount() > 1) { ++mInvalidatedCachedSetAges[cachedSet.getAge()]; } } mLayers.clear(); if (mNewCachedSet) { ++mInvalidatedCachedSetAges[mNewCachedSet->getAge()]; mNewCachedSet = std::nullopt; } } NonBufferHash Flattener::computeLayersHash() const{ size_t hash = 0; for (const auto& layer : mLayers) { android::hashCombineSingleHashed(hash, layer.getNonBufferHash()); } return hash; } // Only called if the geometry matches the last frame. Return true if mLayers // was already populated with these layers, i.e. on the second and following // calls with the same geometry. bool Flattener::mergeWithCachedSets(const std::vector& layers, time_point now) { ATRACE_CALL(); std::vector merged; if (mLayers.empty()) { merged.reserve(layers.size()); for (const LayerState* layer : layers) { merged.emplace_back(layer, now); mFlattenedDisplayCost += merged.back().getDisplayCost(); } mLayers = std::move(merged); return false; } // the compiler should strip out the following no-op loops when ALOGV is off ALOGV("[%s] Incoming layers:", __func__); for (const LayerState* layer : layers) { ALOGV("%s", layer->getName().c_str()); } ALOGV("[%s] Current layers:", __func__); for (const CachedSet& layer : mLayers) { const auto dumper = [&] { std::string dump; layer.dump(dump); return dump; }; ALOGV("%s", dumper().c_str()); } auto currentLayerIter = mLayers.begin(); auto incomingLayerIter = layers.begin(); // If not null, this represents the layer that is blurring the layer before // currentLayerIter. The blurring was stored in the override buffer, so the // layer that requests the blur no longer needs to do any blurring. compositionengine::OutputLayer* priorBlurLayer = nullptr; while (incomingLayerIter != layers.end()) { if (mNewCachedSet && mNewCachedSet->getFirstLayer().getState()->getId() == (*incomingLayerIter)->getId()) { if (mNewCachedSet->hasBufferUpdate()) { ALOGV("[%s] Dropping new cached set", __func__); ++mInvalidatedCachedSetAges[0]; mNewCachedSet = std::nullopt; } else if (mNewCachedSet->hasReadyBuffer()) { ALOGV("[%s] Found ready buffer", __func__); size_t skipCount = mNewCachedSet->getLayerCount(); while (skipCount != 0) { auto* peekThroughLayer = mNewCachedSet->getHolePunchLayer(); const size_t layerCount = currentLayerIter->getLayerCount(); for (size_t i = 0; i < layerCount; ++i) { bool disableBlur = priorBlurLayer && priorBlurLayer == (*incomingLayerIter)->getOutputLayer(); OutputLayer::CompositionState& state = (*incomingLayerIter)->getOutputLayer()->editState(); state.overrideInfo = { .buffer = mNewCachedSet->getBuffer(), .acquireFence = mNewCachedSet->getDrawFence(), .displayFrame = mNewCachedSet->getTextureBounds(), .dataspace = mNewCachedSet->getOutputDataspace(), .displaySpace = mNewCachedSet->getOutputSpace(), .damageRegion = Region::INVALID_REGION, .visibleRegion = mNewCachedSet->getVisibleRegion(), .peekThroughLayer = peekThroughLayer, .disableBackgroundBlur = disableBlur, }; ++incomingLayerIter; } if (currentLayerIter->getLayerCount() > 1) { ++mInvalidatedCachedSetAges[currentLayerIter->getAge()]; } ++currentLayerIter; skipCount -= layerCount; } priorBlurLayer = mNewCachedSet->getBlurLayer(); merged.emplace_back(std::move(*mNewCachedSet)); mNewCachedSet = std::nullopt; continue; } } if (!currentLayerIter->hasBufferUpdate()) { currentLayerIter->incrementAge(); merged.emplace_back(*currentLayerIter); // Skip the incoming layers corresponding to this valid current layer const size_t layerCount = currentLayerIter->getLayerCount(); auto* peekThroughLayer = currentLayerIter->getHolePunchLayer(); for (size_t i = 0; i < layerCount; ++i) { bool disableBlur = priorBlurLayer && priorBlurLayer == (*incomingLayerIter)->getOutputLayer(); OutputLayer::CompositionState& state = (*incomingLayerIter)->getOutputLayer()->editState(); state.overrideInfo = { .buffer = currentLayerIter->getBuffer(), .acquireFence = currentLayerIter->getDrawFence(), .displayFrame = currentLayerIter->getTextureBounds(), .dataspace = currentLayerIter->getOutputDataspace(), .displaySpace = currentLayerIter->getOutputSpace(), .damageRegion = Region(), .visibleRegion = currentLayerIter->getVisibleRegion(), .peekThroughLayer = peekThroughLayer, .disableBackgroundBlur = disableBlur, }; ++incomingLayerIter; } } else if (currentLayerIter->getLayerCount() > 1) { // Break the current layer into its constituent layers ++mInvalidatedCachedSetAges[currentLayerIter->getAge()]; for (CachedSet& layer : currentLayerIter->decompose()) { bool disableBlur = priorBlurLayer && priorBlurLayer == (*incomingLayerIter)->getOutputLayer(); OutputLayer::CompositionState& state = (*incomingLayerIter)->getOutputLayer()->editState(); state.overrideInfo.disableBackgroundBlur = disableBlur; layer.updateAge(now); merged.emplace_back(layer); ++incomingLayerIter; } } else { bool disableBlur = priorBlurLayer && priorBlurLayer == (*incomingLayerIter)->getOutputLayer(); OutputLayer::CompositionState& state = (*incomingLayerIter)->getOutputLayer()->editState(); state.overrideInfo.disableBackgroundBlur = disableBlur; currentLayerIter->updateAge(now); merged.emplace_back(*currentLayerIter); ++incomingLayerIter; } priorBlurLayer = currentLayerIter->getBlurLayer(); ++currentLayerIter; } for (const CachedSet& layer : merged) { mFlattenedDisplayCost += layer.getDisplayCost(); } mLayers = std::move(merged); return true; } std::vector Flattener::findCandidateRuns(time_point now) const { ATRACE_CALL(); std::vector runs; bool isPartOfRun = false; Run::Builder builder; bool firstLayer = true; bool runHasFirstLayer = false; for (auto currentSet = mLayers.cbegin(); currentSet != mLayers.cend(); ++currentSet) { const bool layerIsInactive = now - currentSet->getLastUpdate() > mActiveLayerTimeout; const bool layerHasBlur = currentSet->hasBlurBehind(); if (layerIsInactive && (firstLayer || runHasFirstLayer || !layerHasBlur) && !currentSet->hasUnsupportedDataspace()) { if (isPartOfRun) { builder.append(currentSet->getLayerCount()); } else { // Runs can't start with a non-buffer layer if (currentSet->getFirstLayer().getBuffer() == nullptr) { ALOGV("[%s] Skipping initial non-buffer layer", __func__); } else { builder.init(currentSet); if (firstLayer) { runHasFirstLayer = true; } isPartOfRun = true; } } } else if (isPartOfRun) { builder.setHolePunchCandidate(&(*currentSet)); // If we're here then this blur layer recently had an active buffer updating, meaning // that there is exactly one layer. Blur radius currently is part of layer stack // geometry, so we're also guaranteed that the background blur radius hasn't changed for // at least as long as this new inactive cached set. if (runHasFirstLayer && layerHasBlur && currentSet->getFirstLayer().getBackgroundBlurRadius() > 0) { builder.setBlurringLayer(&(*currentSet)); } if (auto run = builder.validateAndBuild(); run) { runs.push_back(*run); } runHasFirstLayer = false; builder.reset(); isPartOfRun = false; } firstLayer = false; } // If we're in the middle of a run at the end, we still need to validate and build it. if (isPartOfRun) { if (auto run = builder.validateAndBuild(); run) { runs.push_back(*run); } } ALOGV("[%s] Found %zu candidate runs", __func__, runs.size()); return runs; } std::optional Flattener::findBestRun(std::vector& runs) const { if (runs.empty()) { return std::nullopt; } // TODO (b/181192467): Choose the best run, instead of just the first. return runs[0]; } void Flattener::buildCachedSets(time_point now) { ATRACE_CALL(); if (mLayers.empty()) { ALOGV("[%s] No layers found, returning", __func__); return; } // Don't try to build a new cached set if we already have a new one in progress if (mNewCachedSet) { return; } for (const CachedSet& layer : mLayers) { // TODO (b/191997217): make it less aggressive, and sync with findCandidateRuns if (layer.hasProtectedLayers()) { ATRACE_NAME("layer->hasProtectedLayers()"); return; } } std::vector runs = findCandidateRuns(now); std::optional bestRun = findBestRun(runs); if (!bestRun) { return; } mNewCachedSet.emplace(*bestRun->getStart()); mNewCachedSet->setLastUpdate(now); auto currentSet = bestRun->getStart(); while (mNewCachedSet->getLayerCount() < bestRun->getLayerLength()) { ++currentSet; mNewCachedSet->append(*currentSet); } if (bestRun->getBlurringLayer()) { mNewCachedSet->addBackgroundBlurLayer(*bestRun->getBlurringLayer()); } if (mEnableHolePunch && bestRun->getHolePunchCandidate() && bestRun->getHolePunchCandidate()->requiresHolePunch()) { // Add the pip layer to mNewCachedSet, but in a special way - it should // replace the buffer with a clear round rect. mNewCachedSet->addHolePunchLayerIfFeasible(*bestRun->getHolePunchCandidate(), bestRun->getStart() == mLayers.cbegin()); } // TODO(b/181192467): Actually compute new LayerState vector and corresponding hash for each run // and feedback into the predictor ++mCachedSetCreationCount; mCachedSetCreationCost += mNewCachedSet->getCreationCost(); // note the compiler should strip the follow no-op statements when ALOGV is off const auto dumper = [&] { std::string setDump; mNewCachedSet->dump(setDump); return setDump; }; ALOGV("[%s] Added new cached set:\n%s", __func__, dumper().c_str()); } } // namespace android::compositionengine::impl::planner