diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index ff29a5b9e352e..84c0d1369e835 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -19,14 +19,17 @@ #include #include #include +#include #include #include #include #include +#include #include "LightingInfo.h" #include "TreeInfo.h" #include "VectorDrawable.h" #include "thread/CommonPool.h" +#include "tools/SkSharingProc.h" #include "utils/TraceUtils.h" #include @@ -99,7 +102,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) SkASSERT(layerNode->getLayerSurface()); SkiaDisplayList* displayList = (SkiaDisplayList*)layerNode->getDisplayList(); if (!displayList || displayList->isEmpty()) { - SkDEBUGF(("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName())); + ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); return; } @@ -233,58 +236,138 @@ static void savePictureAsync(const sk_sp& data, const std::string& filen if (stream.isValid()) { stream.write(data->data(), data->size()); stream.flush(); - SkDebugf("SKP Captured Drawing Output (%d bytes) for frame. %s", stream.bytesWritten(), + ALOGD("SKP Captured Drawing Output (%zu bytes) for frame. %s", stream.bytesWritten(), filename.c_str()); } }); } -SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) { - if (CC_UNLIKELY(Properties::skpCaptureEnabled)) { +// Note multiple SkiaPipeline instances may be loaded if more than one app is visible. +// Each instance may observe the filename changing and try to record to a file of the same name. +// Only the first one will succeed. There is no scope available here where we could coordinate +// to cause this function to return true for only one of the instances. +bool SkiaPipeline::shouldStartNewFileCapture() { + // Don't start a new file based capture if one is currently ongoing. + if (mCaptureMode != CaptureMode::None) { return false; } + + // A new capture is started when the filename property changes. + // Read the filename property. + std::string prop = base::GetProperty(PROPERTY_CAPTURE_SKP_FILENAME, "0"); + // if the filename property changed to a valid value + if (prop[0] != '0' && mCapturedFile != prop) { + // remember this new filename + mCapturedFile = prop; + // and get a property indicating how many frames to capture. + mCaptureSequence = base::GetIntProperty(PROPERTY_CAPTURE_SKP_FRAMES, 1); if (mCaptureSequence <= 0) { - std::string prop = base::GetProperty(PROPERTY_CAPTURE_SKP_FILENAME, "0"); - if (prop[0] != '0' && mCapturedFile != prop) { - mCapturedFile = prop; - mCaptureSequence = base::GetIntProperty(PROPERTY_CAPTURE_SKP_FRAMES, 1); - } + return false; + } else if (mCaptureSequence == 1) { + mCaptureMode = CaptureMode::SingleFrameSKP; + } else { + mCaptureMode = CaptureMode::MultiFrameSKP; } - if (mCaptureSequence > 0 || mPictureCapturedCallback) { - mRecorder.reset(new SkPictureRecorder()); - SkCanvas* pictureCanvas = - mRecorder->beginRecording(surface->width(), surface->height(), nullptr, - SkPictureRecorder::kPlaybackDrawPicture_RecordFlag); - mNwayCanvas = std::make_unique(surface->width(), surface->height()); - mNwayCanvas->addCanvas(surface->getCanvas()); - mNwayCanvas->addCanvas(pictureCanvas); - return mNwayCanvas.get(); + return true; + } + return false; +} + +// performs the first-frame work of a multi frame SKP capture. Returns true if successful. +bool SkiaPipeline::setupMultiFrameCapture() { + ALOGD("Set up multi-frame capture, frames = %d", mCaptureSequence); + // We own this stream and need to hold it until close() finishes. + auto stream = std::make_unique(mCapturedFile.c_str()); + if (stream->isValid()) { + mOpenMultiPicStream = std::move(stream); + mSerialContext.reset(new SkSharingSerialContext()); + SkSerialProcs procs; + procs.fImageProc = SkSharingSerialContext::serializeImage; + procs.fImageCtx = mSerialContext.get(); + // SkDocuments don't take owership of the streams they write. + // we need to keep it until after mMultiPic.close() + // procs is passed as a pointer, but just as a method of having an optional default. + // procs doesn't need to outlive this Make call. + mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs); + return true; + } else { + ALOGE("Could not open \"%s\" for writing.", mCapturedFile.c_str()); + mCaptureSequence = 0; + mCaptureMode = CaptureMode::None; + return false; + } +} + +SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) { + if (CC_LIKELY(!Properties::skpCaptureEnabled)) { + return surface->getCanvas(); // Bail out early when capture is not turned on. + } + // Note that shouldStartNewFileCapture tells us if this is the *first* frame of a capture. + if (shouldStartNewFileCapture() && mCaptureMode == CaptureMode::MultiFrameSKP) { + if (!setupMultiFrameCapture()) { + return surface->getCanvas(); } } - return surface->getCanvas(); + + // Create a canvas pointer, fill it depending on what kind of capture is requested (if any) + SkCanvas* pictureCanvas = nullptr; + switch (mCaptureMode) { + case CaptureMode::CallbackAPI: + case CaptureMode::SingleFrameSKP: + mRecorder.reset(new SkPictureRecorder()); + pictureCanvas = mRecorder->beginRecording(surface->width(), surface->height(), + nullptr, SkPictureRecorder::kPlaybackDrawPicture_RecordFlag); + break; + case CaptureMode::MultiFrameSKP: + // If a multi frame recording is active, initialize recording for a single frame of a + // multi frame file. + pictureCanvas = mMultiPic->beginPage(surface->width(), surface->height()); + break; + case CaptureMode::None: + // Returning here in the non-capture case means we can count on pictureCanvas being + // non-null below. + return surface->getCanvas(); + } + + // Setting up an nway canvas is common to any kind of capture. + mNwayCanvas = std::make_unique(surface->width(), surface->height()); + mNwayCanvas->addCanvas(surface->getCanvas()); + mNwayCanvas->addCanvas(pictureCanvas); + return mNwayCanvas.get(); } void SkiaPipeline::endCapture(SkSurface* surface) { + if (CC_LIKELY(mCaptureMode == CaptureMode::None)) { return; } mNwayCanvas.reset(); - if (CC_UNLIKELY(mRecorder.get())) { - ATRACE_CALL(); + ATRACE_CALL(); + if (mCaptureSequence > 0 && mCaptureMode == CaptureMode::MultiFrameSKP) { + mMultiPic->endPage(); + mCaptureSequence--; + if (mCaptureSequence == 0) { + mCaptureMode = CaptureMode::None; + // Pass mMultiPic and mOpenMultiPicStream to a background thread, which will handle + // the heavyweight serialization work and destroy them. mOpenMultiPicStream is released + // to a bare pointer because keeping it in a smart pointer makes the lambda + // non-copyable. The lambda is only called once, so this is safe. + SkFILEWStream* stream = mOpenMultiPicStream.release(); + CommonPool::post([doc = std::move(mMultiPic), stream]{ + ALOGD("Finalizing multi frame SKP"); + doc->close(); + delete stream; + ALOGD("Multi frame SKP complete."); + }); + } + } else { sk_sp picture = mRecorder->finishRecordingAsPicture(); if (picture->approximateOpCount() > 0) { - if (mCaptureSequence > 0) { - ATRACE_BEGIN("picture->serialize"); - auto data = picture->serialize(); - ATRACE_END(); - - // offload saving to file in a different thread - if (1 == mCaptureSequence) { - savePictureAsync(data, mCapturedFile); - } else { - savePictureAsync(data, mCapturedFile + "_" + std::to_string(mCaptureSequence)); - } - mCaptureSequence--; - } if (mPictureCapturedCallback) { std::invoke(mPictureCapturedCallback, std::move(picture)); + } else { + // single frame skp to file + auto data = picture->serialize(); + savePictureAsync(data, mCapturedFile); + mCaptureSequence = 0; } } + mCaptureMode = CaptureMode::None; mRecorder.reset(); } } @@ -305,7 +388,6 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli // initialize the canvas for the current frame, that might be a recording canvas if SKP // capture is enabled. - std::unique_ptr recorder; SkCanvas* canvas = tryCapture(surface.get()); renderFrameImpl(layers, clip, nodes, opaque, contentDrawBounds, canvas, preTransform); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index 5fc1d6169d4a8..37b559f92f05f 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -17,12 +17,15 @@ #pragma once #include +#include +#include #include "Lighting.h" #include "hwui/AnimatedImageDrawable.h" #include "renderthread/CanvasContext.h" #include "renderthread/IRenderPipeline.h" class SkPictureRecorder; +struct SkSharingSerialContext; namespace android { namespace uirenderer { @@ -60,9 +63,12 @@ public: void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque); + // Sets the recording callback to the provided function and the recording mode + // to CallbackAPI void setPictureCapturedCallback( const std::function&&)>& callback) override { mPictureCapturedCallback = callback; + mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None; } protected: @@ -92,8 +98,18 @@ private: */ void renderVectorDrawableCache(); + // Called every frame. Normally returns early with screen canvas. + // But when capture is enabled, returns an nwaycanvas where commands are also recorded. SkCanvas* tryCapture(SkSurface* surface); + // Called at the end of every frame, closes the recording if necessary. void endCapture(SkSurface* surface); + // Determine if a new file-based capture should be started. + // If so, sets mCapturedFile and mCaptureSequence and returns true. + // Should be called every frame when capture is enabled. + // sets mCaptureMode. + bool shouldStartNewFileCapture(); + // Set up a multi frame capture. + bool setupMultiFrameCapture(); std::vector> mPinnedImages; @@ -103,22 +119,46 @@ private: std::vector mVectorDrawables; // Block of properties used only for debugging to record a SkPicture and save it in a file. + // There are three possible ways of recording drawing commands. + enum class CaptureMode { + // return to this mode when capture stops. + None, + // A mode where every frame is recorded into an SkPicture and sent to a provided callback, + // until that callback is cleared + CallbackAPI, + // A mode where a finite number of frames are recorded to a file with + // SkMultiPictureDocument + MultiFrameSKP, + // A mode which records a single frame to a normal SKP file. + SingleFrameSKP, + }; + CaptureMode mCaptureMode = CaptureMode::None; + /** - * mCapturedFile is used to enforce we don't capture more than once for a given name (cause - * permissions don't allow to reset a property from render thread). + * mCapturedFile - the filename to write a recorded SKP to in either MultiFrameSKP or + * SingleFrameSKP mode. */ std::string mCapturedFile; /** - * mCaptureSequence counts how many frames are left to take in the sequence. + * mCaptureSequence counts down how many frames are left to take in the sequence. Applicable + * only to MultiFrameSKP or SingleFrameSKP mode. */ int mCaptureSequence = 0; + // Multi frame serialization stream and writer used when serializing more than one frame. + std::unique_ptr mOpenMultiPicStream; + sk_sp mMultiPic; + std::unique_ptr mSerialContext; + /** - * mRecorder holds the current picture recorder. We could store it on the stack to support - * parallel tryCapture calls (not really needed). + * mRecorder holds the current picture recorder when serializing in either SingleFrameSKP or + * CallbackAPI modes. */ std::unique_ptr mRecorder; std::unique_ptr mNwayCanvas; + + // Set by setPictureCapturedCallback and when set, CallbackAPI mode recording is ongoing. + // Not used in other recording modes. std::function&&)> mPictureCapturedCallback; }; diff --git a/libs/hwui/tests/scripts/skp-capture.sh b/libs/hwui/tests/scripts/skp-capture.sh index 54fa229295861..aad31fcc8eb92 100755 --- a/libs/hwui/tests/scripts/skp-capture.sh +++ b/libs/hwui/tests/scripts/skp-capture.sh @@ -4,6 +4,12 @@ # # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +# +# Before this can be used, the device must be rooted and the filesystem must be writable by Skia +# - These steps are necessary once after flashing to enable capture - +# adb root +# adb remount +# adb reboot if [ -z "$1" ]; then printf 'Usage:\n skp-capture.sh PACKAGE_NAME OPTIONAL_FRAME_COUNT\n\n' @@ -20,8 +26,8 @@ if ! command -v adb > /dev/null 2>&1; then exit 2 fi fi -phase1_timeout_seconds=15 -phase2_timeout_seconds=60 +phase1_timeout_seconds=60 +phase2_timeout_seconds=300 package="$1" filename="$(date '+%H%M%S').skp" remote_path="/data/data/${package}/cache/${filename}" @@ -29,11 +35,14 @@ local_path_prefix="$(date '+%Y-%m-%d_%H%M%S')_${package}" local_path="${local_path_prefix}.skp" enable_capture_key='debug.hwui.capture_skp_enabled' enable_capture_value=$(adb shell "getprop '${enable_capture_key}'") -#printf 'captureflag=' "$enable_capture_value" '\n' + +# TODO(nifong): check if filesystem is writable here with "avbctl get-verity" +# result will either start with "verity is disabled" or "verity is enabled" + if [ -z "$enable_capture_value" ]; then - printf 'Capture SKP property need to be enabled first. Please use\n' - printf "\"adb shell setprop debug.hwui.capture_skp_enabled true\" and then restart\n" - printf "the process.\n\n" + printf 'debug.hwui.capture_skp_enabled was found to be disabled, enabling it now.\n' + printf " restart the process you want to capture on the device, then retry this script.\n\n" + adb shell "setprop '${enable_capture_key}' true" exit 1 fi if [ ! -z "$2" ]; then @@ -57,33 +66,17 @@ banner() { printf ' %s' "$*" printf '\n=====================\n' } -banner '...WAITING...' -adb_test_exist() { - test '0' = "$(adb shell "test -e \"$1\"; echo \$?")"; -} -timeout=$(( $(date +%s) + $phase1_timeout_seconds)) -while ! adb_test_exist "$remote_path"; do - spin 0.05 - if [ $(date +%s) -gt $timeout ] ; then - printf '\bTimed out.\n' - adb shell "setprop '${filename_key}' ''" - exit 3 - fi -done -printf '\b' - -#read -n1 -r -p "Press any key to continue..." key - -banner '...SAVING...' +banner '...WAITING FOR APP INTERACTION...' +# Waiting for nonzero file is an indication that the pipeline has both opened the file and written +# the header. With multiple frames this does not occur until the last frame has been recorded, +# so we continue to show the "waiting for app interaction" message as long as the app still requires +# interaction to draw more frames. adb_test_file_nonzero() { # grab first byte of `du` output X="$(adb shell "du \"$1\" 2> /dev/null | dd bs=1 count=1 2> /dev/null")" test "$X" && test "$X" -ne 0 } -#adb_filesize() { -# adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}' -#} -timeout=$(( $(date +%s) + $phase2_timeout_seconds)) +timeout=$(( $(date +%s) + $phase1_timeout_seconds)) while ! adb_test_file_nonzero "$remote_path"; do spin 0.05 if [ $(date +%s) -gt $timeout ] ; then @@ -94,8 +87,37 @@ while ! adb_test_file_nonzero "$remote_path"; do done printf '\b' +# Disable further capturing adb shell "setprop '${filename_key}' ''" +banner '...SAVING...' +# return the size of a file in bytes +adb_filesize() { + adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}' +} +timeout=$(( $(date +%s) + $phase2_timeout_seconds)) +last_size='0' # output of last size check command +unstable=true # false once the file size stops changing +counter=0 # used to perform size check only 1/sec though we update spinner 20/sec +# loop until the file size is unchanged for 1 second. +while [ $unstable != 0 ] ; do + spin 0.05 + counter=$(( $counter+1 )) + if ! (( $counter % 20)) ; then + new_size=$(adb_filesize "$remote_path") + unstable=$(($(adb_filesize "$remote_path") != last_size)) + last_size=$new_size + fi + if [ $(date +%s) -gt $timeout ] ; then + printf '\bTimed out.\n' + adb shell "setprop '${filename_key}' ''" + exit 3 + fi +done +printf '\b' + +printf "SKP file serialized: %s\n" $(echo $last_size | numfmt --to=iec) + i=0; while [ $i -lt 10 ]; do spin 0.10; i=$(($i + 1)); done; echo adb pull "$remote_path" "$local_path" @@ -105,12 +127,4 @@ if ! [ -f "$local_path" ] ; then fi adb shell rm "$remote_path" printf '\nSKP saved to file:\n %s\n\n' "$local_path" -if [ ! -z "$2" ]; then - bridge="_" - adb shell "setprop 'debug.hwui.capture_skp_frames' ''" - for i in $(seq 2 $2); do - adb pull "${remote_path}_${i}" "${local_path_prefix}_${i}.skp" - adb shell rm "${remote_path}_${i}" - done -fi