| /* | |
| * 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 <android-base/properties.h> | |
| #include <compositionengine/impl/planner/Flattener.h> | |
| #include <compositionengine/impl/planner/LayerState.h> | |
| #include <gui/TraceUtils.h> | |
| 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<const LayerState*>& incomingLayers, | |
| const std::vector<CachedSet>& cachedSets) { | |
| std::vector<const LayerState*> 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, const Tunables& tunables) | |
| : mRenderEngine(renderEngine), mTunables(tunables), mTexturePool(mRenderEngine) {} | |
| NonBufferHash Flattener::flattenLayers(const std::vector<const LayerState*>& 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<std::chrono::steady_clock::time_point> 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 && mTunables.mRenderScheduling) { | |
| if (const auto estimatedRenderFinish = | |
| now + mTunables.mRenderScheduling->cachedSetRenderDuration; | |
| estimatedRenderFinish > *renderDeadline) { | |
| mNewCachedSet->incrementSkipCount(); | |
| if (mNewCachedSet->getSkipCount() <= | |
| mTunables.mRenderScheduling->maxDeferRenderAttempts) { | |
| ATRACE_FORMAT("DeadlinePassed: exceeded deadline by: %d us", | |
| std::chrono::duration_cast<std::chrono::microseconds>( | |
| 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<size_t>(mDisplaySize.width * mDisplaySize.height); | |
| base::StringAppendF(&result, " Unflattened: %.2f\n", | |
| static_cast<float>(mUnflattenedDisplayCost) / displayArea); | |
| base::StringAppendF(&result, " Flattened: %.2f\n", | |
| static_cast<float>(mFlattenedDisplayCost) / displayArea); | |
| const auto compareLayerCounts = [](const std::pair<size_t, size_t>& left, | |
| const std::pair<size_t, size_t>& 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<float>(mCachedSetCreationCost) / displayArea); | |
| const auto lastUpdate = | |
| std::chrono::duration_cast<std::chrono::milliseconds>(now - mLastGeometryUpdate); | |
| base::StringAppendF(&result, "\n Current hash %016zx, last update %sago\n\n", mCurrentGeometry, | |
| durationString(lastUpdate).c_str()); | |
| dumpLayers(result); | |
| base::StringAppendF(&result, "\n"); | |
| mTexturePool.dump(result); | |
| } | |
| size_t Flattener::calculateDisplayCost(const std::vector<const LayerState*>& 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<size_t>(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<size_t>(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<const LayerState*>& layers, time_point now) { | |
| ATRACE_CALL(); | |
| std::vector<CachedSet> 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->getBounds(), | |
| .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->getBounds(), | |
| .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::Run> Flattener::findCandidateRuns(time_point now) const { | |
| ATRACE_CALL(); | |
| std::vector<Run> 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() > mTunables.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::Run> Flattener::findBestRun(std::vector<Flattener::Run>& 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<Run> runs = findCandidateRuns(now); | |
| std::optional<Run> 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 (mTunables.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 |