From e9d0012bbf8077c1b7e09e44ae30e7873eb7ac31 Mon Sep 17 00:00:00 2001 From: Stan Iliev Date: Tue, 19 Sep 2017 12:07:10 -0400 Subject: [PATCH] Convert skpCaptureEnabled compile time flag into a property Convert SkiaPipeline::skpCaptureEnabled into a system property. Add ability to capture drawing in layers. Add ability to capture animations/sequence of frames. Fix crash when recording a TextureView. Test: Ran capture script. Change-Id: I463eecf6ec90a601a6cc172ad1901bd4bcc86ac8 --- libs/hwui/Properties.cpp | 4 + libs/hwui/Properties.h | 18 +++ libs/hwui/pipeline/skia/LayerDrawable.cpp | 4 + .../hwui/pipeline/skia/RenderNodeDrawable.cpp | 2 +- libs/hwui/pipeline/skia/SkiaPipeline.cpp | 124 +++++++++++++----- libs/hwui/pipeline/skia/SkiaPipeline.h | 32 ++++- libs/hwui/tests/scripts/skp-capture.sh | 116 ++++++++++++++++ .../tests/unit/RenderNodeDrawableTests.cpp | 6 +- 8 files changed, 270 insertions(+), 36 deletions(-) create mode 100755 libs/hwui/tests/scripts/skp-capture.sh diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 3b59bb1a8f02b..588c4dea998fb 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -58,6 +58,7 @@ bool Properties::forceDrawFrame = false; bool Properties::filterOutTestOverhead = false; bool Properties::disableVsync = false; +bool Properties::skpCaptureEnabled = false; static int property_get_int(const char* key, int defaultValue) { char buf[PROPERTY_VALUE_MAX] = {'\0',}; @@ -128,6 +129,9 @@ bool Properties::load() { filterOutTestOverhead = property_get_bool(PROPERTY_FILTER_TEST_OVERHEAD, false); + skpCaptureEnabled = property_get_bool("ro.debuggable", false) + && property_get_bool(PROPERTY_CAPTURE_SKP_ENABLED, false); + return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw) || (prevDebugStencilClip != debugStencilClip); diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 0fe4c77147613..077e7aee65792 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -165,6 +165,22 @@ enum DebugLevel { */ #define PROPERTY_RENDERER "debug.hwui.renderer" +/** + * Allows to collect a recording of Skia drawing commands. + */ +#define PROPERTY_CAPTURE_SKP_ENABLED "debug.hwui.capture_skp_enabled" + + +/** + * Defines how many frames in a sequence to capture. + */ +#define PROPERTY_CAPTURE_SKP_FRAMES "debug.hwui.capture_skp_frames" + +/** + * File name and location, where a SKP recording will be saved. + */ +#define PROPERTY_CAPTURE_SKP_FILENAME "debug.hwui.skp_filename" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -254,6 +270,8 @@ public: // created after changing this. static bool disableVsync; + static bool skpCaptureEnabled; + // Used for testing only to change the render pipeline. #ifdef HWUI_GLES_WRAP_ENABLED static void overrideRenderPipelineType(RenderPipelineType); diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 3b72a8b3f7fc3..def04e11d5d87 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -35,6 +35,10 @@ void LayerDrawable::onDraw(SkCanvas* canvas) { } bool LayerDrawable::DrawLayer(GrContext* context, SkCanvas* canvas, Layer* layer) { + if (context == nullptr) { + SkDEBUGF(("Attempting to draw LayerDrawable into an unsupported surface")); + return false; + } // transform the matrix based on the layer SkMatrix layerTransform; layer->getTransform().copyTo(layerTransform); diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp index 47dee9d4e9d84..ca93925c3c04d 100644 --- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp +++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp @@ -101,7 +101,7 @@ void RenderNodeDrawable::onDraw(SkCanvas* canvas) { void RenderNodeDrawable::forceDraw(SkCanvas* canvas) { RenderNode* renderNode = mRenderNode.get(); - if (SkiaPipeline::skpCaptureEnabled()) { + if (CC_UNLIKELY(Properties::skpCaptureEnabled)) { SkRect dimensions = SkRect::MakeWH(renderNode->getWidth(), renderNode->getHeight()); canvas->drawAnnotation(dimensions, renderNode->getName(), nullptr); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index c4bd1e1b10d43..88ac10a7b0723 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -106,7 +106,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, const Rect& layerDamage = layers.entries()[i].damage; - SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); + SkCanvas* layerCanvas = tryCapture(layerNode->getLayerSurface()); int saveCount = layerCanvas->save(); SkASSERT(saveCount == 1); @@ -133,9 +133,11 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, layerCanvas->restoreToCount(saveCount); mLightCenter = savedLightCenter; + endCapture(layerNode->getLayerSurface()); + // cache the current context so that we can defer flushing it until // either all the layers have been rendered or the context changes - GrContext* currentContext = layerCanvas->getGrContext(); + GrContext* currentContext = layerNode->getLayerSurface()->getCanvas()->getGrContext(); if (cachedContext.get() != currentContext) { if (cachedContext.get()) { cachedContext->flush(); @@ -221,6 +223,91 @@ void SkiaPipeline::renderVectorDrawableCache() { } } +class SkiaPipeline::SavePictureProcessor : public TaskProcessor { +public: + explicit SavePictureProcessor(TaskManager* taskManager) : TaskProcessor(taskManager) {} + + struct SavePictureTask : public Task { + sk_sp data; + std::string filename; + }; + + void savePicture(const sk_sp& data, const std::string& filename) { + sp task(new SavePictureTask()); + task->data = data; + task->filename = filename; + TaskProcessor::add(task); + } + + virtual void onProcess(const sp >& task) override { + SavePictureTask* t = static_cast(task.get()); + + if (0 == access(t->filename.c_str(), F_OK)) { + task->setResult(false); + return; + } + + SkFILEWStream stream(t->filename.c_str()); + if (stream.isValid()) { + stream.write(t->data->data(), t->data->size()); + stream.flush(); + SkDebugf("SKP Captured Drawing Output (%d bytes) for frame. %s", + stream.bytesWritten(), t->filename.c_str()); + } + + task->setResult(true); + } +}; + +SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) { + if (CC_UNLIKELY(Properties::skpCaptureEnabled)) { + bool recordingPicture = mCaptureSequence > 0; + char prop[PROPERTY_VALUE_MAX] = {'\0'}; + if (!recordingPicture) { + property_get(PROPERTY_CAPTURE_SKP_FILENAME, prop, "0"); + recordingPicture = prop[0] != '0' + && mCapturedFile != prop; //ensure we capture only once per filename + if (recordingPicture) { + mCapturedFile = prop; + mCaptureSequence = property_get_int32(PROPERTY_CAPTURE_SKP_FRAMES, 1); + } + } + if (recordingPicture) { + mRecorder.reset(new SkPictureRecorder()); + return mRecorder->beginRecording(surface->width(), surface->height(), nullptr, + SkPictureRecorder::kPlaybackDrawPicture_RecordFlag); + } + } + return surface->getCanvas(); +} + +void SkiaPipeline::endCapture(SkSurface* surface) { + if (CC_UNLIKELY(mRecorder.get())) { + sk_sp picture = mRecorder->finishRecordingAsPicture(); + surface->getCanvas()->drawPicture(picture); + if (picture->approximateOpCount() > 0) { + SkDynamicMemoryWStream stream; + PngPixelSerializer serializer; + picture->serialize(&stream, &serializer); + + //offload saving to file in a different thread + if (!mSavePictureProcessor.get()) { + TaskManager* taskManager = getTaskManager(); + mSavePictureProcessor = new SavePictureProcessor( + taskManager->canRunTasks() ? taskManager : nullptr); + } + if (1 == mCaptureSequence) { + mSavePictureProcessor->savePicture(stream.detachAsData(), mCapturedFile); + } else { + mSavePictureProcessor->savePicture(stream.detachAsData(), + mCapturedFile + "_" + std::to_string(mCaptureSequence)); + } + mCaptureSequence--; + } + mRecorder.reset(); + } +} + void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip, const std::vector>& nodes, bool opaque, bool wideColorGamut, const Rect &contentDrawBounds, sk_sp surface) { @@ -230,44 +317,21 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli // draw all layers up front renderLayersImpl(layers, opaque, wideColorGamut); - // initialize the canvas for the current frame - SkCanvas* canvas = surface->getCanvas(); - + // initialize the canvas for the current frame, that might be a recording canvas if SKP + // capture is enabled. std::unique_ptr recorder; - bool recordingPicture = false; - char prop[PROPERTY_VALUE_MAX]; - if (skpCaptureEnabled()) { - property_get("debug.hwui.capture_frame_as_skp", prop, "0"); - recordingPicture = prop[0] != '0' && access(prop, F_OK) != 0; - if (recordingPicture) { - recorder.reset(new SkPictureRecorder()); - canvas = recorder->beginRecording(surface->width(), surface->height(), - nullptr, SkPictureRecorder::kPlaybackDrawPicture_RecordFlag); - } - } + SkCanvas* canvas = tryCapture(surface.get()); renderFrameImpl(layers, clip, nodes, opaque, wideColorGamut, contentDrawBounds, canvas); - if (skpCaptureEnabled() && recordingPicture) { - sk_sp picture = recorder->finishRecordingAsPicture(); - if (picture->approximateOpCount() > 0) { - SkFILEWStream stream(prop); - if (stream.isValid()) { - PngPixelSerializer serializer; - picture->serialize(&stream, &serializer); - stream.flush(); - SkDebugf("Captured Drawing Output (%d bytes) for frame. %s", stream.bytesWritten(), prop); - } - } - surface->getCanvas()->drawPicture(picture); - } + endCapture(surface.get()); if (CC_UNLIKELY(Properties::debugOverdraw)) { renderOverdraw(layers, clip, nodes, contentDrawBounds, surface); } ATRACE_NAME("flush commands"); - canvas->flush(); + surface->getCanvas()->flush(); } namespace { diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index 3e6ae306217ca..d1faa8a4ccb56 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -21,6 +21,8 @@ #include "renderthread/IRenderPipeline.h" #include +class SkPictureRecorder; + namespace android { namespace uirenderer { namespace skiapipeline { @@ -55,9 +57,7 @@ public: static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); - static void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque, bool wideColorGamut); - - static bool skpCaptureEnabled() { return false; } + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque, bool wideColorGamut); static float getLightRadius() { if (CC_UNLIKELY(Properties::overrideLightRadius > 0)) { @@ -126,12 +126,38 @@ private: */ void renderVectorDrawableCache(); + SkCanvas* tryCapture(SkSurface* surface); + void endCapture(SkSurface* surface); + std::vector> mPinnedImages; /** * populated by prepareTree with dirty VDs */ std::vector mVectorDrawables; + + + // Block of properties used only for debugging to record a SkPicture and save it in a file. + /** + * 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). + */ + std::string mCapturedFile; + /** + * mCaptureSequence counts how many frames are left to take in the sequence. + */ + int mCaptureSequence = 0; + /** + * mSavePictureProcessor is used to run the file saving code in a separate thread. + */ + class SavePictureProcessor; + sp mSavePictureProcessor; + /** + * mRecorder holds the current picture recorder. We could store it on the stack to support + * parallel tryCapture calls (not really needed). + */ + std::unique_ptr mRecorder; + static float mLightRadius; static uint8_t mAmbientShadowAlpha; static uint8_t mSpotShadowAlpha; diff --git a/libs/hwui/tests/scripts/skp-capture.sh b/libs/hwui/tests/scripts/skp-capture.sh new file mode 100755 index 0000000000000..54fa229295861 --- /dev/null +++ b/libs/hwui/tests/scripts/skp-capture.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +# Copyright 2015 Google Inc. +# +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if [ -z "$1" ]; then + printf 'Usage:\n skp-capture.sh PACKAGE_NAME OPTIONAL_FRAME_COUNT\n\n' + printf "Use \`adb shell 'pm list packages'\` to get a listing.\n\n" + exit 1 +fi +if ! command -v adb > /dev/null 2>&1; then + if [ -x "${ANDROID_SDK_ROOT}/platform-tools/adb" ]; then + adb() { + "${ANDROID_SDK_ROOT}/platform-tools/adb" "$@" + } + else + echo 'adb missing' + exit 2 + fi +fi +phase1_timeout_seconds=15 +phase2_timeout_seconds=60 +package="$1" +filename="$(date '+%H%M%S').skp" +remote_path="/data/data/${package}/cache/${filename}" +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' +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" + exit 1 +fi +if [ ! -z "$2" ]; then + adb shell "setprop 'debug.hwui.capture_skp_frames' $2" +fi +filename_key='debug.hwui.skp_filename' +adb shell "setprop '${filename_key}' '${remote_path}'" +spin() { + case "$spin" in + 1) printf '\b|';; + 2) printf '\b\\';; + 3) printf '\b-';; + *) printf '\b/';; + esac + spin=$(( ( ${spin:-0} + 1 ) % 4 )) + sleep $1 +} + +banner() { + printf '\n=====================\n' + 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...' +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)) +while ! adb_test_file_nonzero "$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' + +adb shell "setprop '${filename_key}' ''" + +i=0; while [ $i -lt 10 ]; do spin 0.10; i=$(($i + 1)); done; echo + +adb pull "$remote_path" "$local_path" +if ! [ -f "$local_path" ] ; then + printf "something went wrong with `adb pull`." + exit 4 +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 + diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp index 4c3e182ced2f8..d182d78da00ad 100644 --- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp @@ -22,6 +22,7 @@ #include "IContextFactory.h" #include "pipeline/skia/SkiaDisplayList.h" #include "pipeline/skia/SkiaPipeline.h" +#include "pipeline/skia/SkiaOpenGLPipeline.h" #include "pipeline/skia/SkiaRecordingCanvas.h" #include "renderthread/CanvasContext.h" #include "tests/common/TestUtils.h" @@ -335,7 +336,7 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionReorder) { EXPECT_EQ(3, canvas.getIndex()); } -RENDERTHREAD_TEST(RenderNodeDrawable, projectionHwLayer) { +RENDERTHREAD_SKIA_PIPELINE_TEST(RenderNodeDrawable, projectionHwLayer) { /* R is backward projected on B and C is a layer. A / \ @@ -450,7 +451,8 @@ RENDERTHREAD_TEST(RenderNodeDrawable, projectionHwLayer) { LayerUpdateQueue layerUpdateQueue; layerUpdateQueue.enqueueLayerWithDamage(child.get(), android::uirenderer::Rect(LAYER_WIDTH, LAYER_HEIGHT)); - SkiaPipeline::renderLayersImpl(layerUpdateQueue, true, false); + auto pipeline = std::make_unique(renderThread); + pipeline->renderLayersImpl(layerUpdateQueue, true, false); EXPECT_EQ(1, drawCounter); //assert index 0 is drawn on the layer RenderNodeDrawable drawable(parent.get(), surfaceLayer1->getCanvas(), true);