Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3af61ed7 authored by Nathaniel Nifong's avatar Nathaniel Nifong Committed by Android (Google) Code Review
Browse files

Merge "Introduce multi-frame SKP capturing in SkiaPipeline"

parents 9f2ce7c6 d2e49a29
Loading
Loading
Loading
Loading
+117 −35
Original line number Diff line number Diff line
@@ -19,14 +19,17 @@
#include <SkImageEncoder.h>
#include <SkImageInfo.h>
#include <SkImagePriv.h>
#include <SkMultiPictureDocument.h>
#include <SkOverdrawCanvas.h>
#include <SkOverdrawColorFilter.h>
#include <SkPicture.h>
#include <SkPictureRecorder.h>
#include <SkSerialProcs.h>
#include "LightingInfo.h"
#include "TreeInfo.h"
#include "VectorDrawable.h"
#include "thread/CommonPool.h"
#include "tools/SkSharingProc.h"
#include "utils/TraceUtils.h"

#include <unistd.h>
@@ -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<SkData>& 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)) {
        if (mCaptureSequence <= 0) {
// 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) {
            return false;
        } else if (mCaptureSequence == 1) {
            mCaptureMode = CaptureMode::SingleFrameSKP;
        } else {
            mCaptureMode = CaptureMode::MultiFrameSKP;
        }
        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<SkFILEWStream>(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();
        }
    }
        if (mCaptureSequence > 0 || mPictureCapturedCallback) {

    // 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());
            SkCanvas* pictureCanvas =
                    mRecorder->beginRecording(surface->width(), surface->height(), nullptr,
                                              SkPictureRecorder::kPlaybackDrawPicture_RecordFlag);
            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<SkNWayCanvas>(surface->width(), surface->height());
    mNwayCanvas->addCanvas(surface->getCanvas());
    mNwayCanvas->addCanvas(pictureCanvas);
    return mNwayCanvas.get();
}
    }
    return surface->getCanvas();
}

void SkiaPipeline::endCapture(SkSurface* surface) {
    if (CC_LIKELY(mCaptureMode == CaptureMode::None)) { return; }
    mNwayCanvas.reset();
    if (CC_UNLIKELY(mRecorder.get())) {
    ATRACE_CALL();
        sk_sp<SkPicture> 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));
                }
    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<SkPicture> picture = mRecorder->finishRecordingAsPicture();
        if (picture->approximateOpCount() > 0) {
            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<SkPictureRecorder> recorder;
    SkCanvas* canvas = tryCapture(surface.get());

    renderFrameImpl(layers, clip, nodes, opaque, contentDrawBounds, canvas, preTransform);
+45 −5
Original line number Diff line number Diff line
@@ -17,12 +17,15 @@
#pragma once

#include <SkSurface.h>
#include <SkDocument.h>
#include <SkMultiPictureDocument.h>
#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<void(sk_sp<SkPicture>&&)>& 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<sk_sp<SkImage>> mPinnedImages;

@@ -103,22 +119,46 @@ private:
    std::vector<VectorDrawableRoot*> 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<SkFILEWStream> mOpenMultiPicStream;
    sk_sp<SkDocument> mMultiPic;
    std::unique_ptr<SkSharingSerialContext> 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<SkPictureRecorder> mRecorder;
    std::unique_ptr<SkNWayCanvas> mNwayCanvas;

    // Set by setPictureCapturedCallback and when set, CallbackAPI mode recording is ongoing.
    // Not used in other recording modes.
    std::function<void(sk_sp<SkPicture>&&)> mPictureCapturedCallback;
};

+42 −28
Original line number Diff line number Diff line
@@ -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,12 +66,18 @@ banner() {
    printf '   %s' "$*"
    printf '\n=====================\n'
}
banner '...WAITING...'
adb_test_exist() {
    test '0' = "$(adb shell "test -e \"$1\"; echo \$?")";
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
}
timeout=$(( $(date +%s) + $phase1_timeout_seconds))
while ! adb_test_exist "$remote_path"; do
while ! adb_test_file_nonzero "$remote_path"; do
    spin 0.05
    if [ $(date +%s) -gt $timeout ] ; then
        printf '\bTimed out.\n'
@@ -72,20 +87,27 @@ while ! adb_test_exist "$remote_path"; do
done
printf '\b'

#read -n1 -r -p "Press any key to continue..." key
# Disable further capturing
adb shell "setprop '${filename_key}' ''"

banner '...SAVING...'
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
# return the size of a file in bytes
adb_filesize() {
    adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}'
}
#adb_filesize() {
#    adb shell "wc -c \"$1\"" 2> /dev/null | awk '{print $1}'
#}
timeout=$(( $(date +%s) + $phase2_timeout_seconds))
while ! adb_test_file_nonzero "$remote_path"; do
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}' ''"
@@ -94,7 +116,7 @@ while ! adb_test_file_nonzero "$remote_path"; do
done
printf '\b'

adb shell "setprop '${filename_key}' ''"
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

@@ -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