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
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 3b59bb1..588c4de 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -58,6 +58,7 @@
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 @@
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 0fe4c77..077e7ae 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -165,6 +165,22 @@
*/
#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 @@
// 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 3b72a8b..def04e1 100644
--- a/libs/hwui/pipeline/skia/LayerDrawable.cpp
+++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp
@@ -35,6 +35,10 @@
}
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 47dee9d..ca93925 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -101,7 +101,7 @@
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 c4bd1e1..88ac10a 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -106,7 +106,7 @@
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 @@
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 @@
}
}
+class SkiaPipeline::SavePictureProcessor : public TaskProcessor<bool> {
+public:
+ explicit SavePictureProcessor(TaskManager* taskManager) : TaskProcessor<bool>(taskManager) {}
+
+ struct SavePictureTask : public Task<bool> {
+ sk_sp<SkData> data;
+ std::string filename;
+ };
+
+ void savePicture(const sk_sp<SkData>& data, const std::string& filename) {
+ sp<SavePictureTask> task(new SavePictureTask());
+ task->data = data;
+ task->filename = filename;
+ TaskProcessor<bool>::add(task);
+ }
+
+ virtual void onProcess(const sp<Task<bool> >& task) override {
+ SavePictureTask* t = static_cast<SavePictureTask*>(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<SkPicture> 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<sp<RenderNode>>& nodes, bool opaque, bool wideColorGamut,
const Rect &contentDrawBounds, sk_sp<SkSurface> surface) {
@@ -230,44 +317,21 @@
// 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<SkPictureRecorder> 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<SkPicture> 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 3e6ae30..d1faa8a 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -21,6 +21,8 @@
#include "renderthread/IRenderPipeline.h"
#include <SkSurface.h>
+class SkPictureRecorder;
+
namespace android {
namespace uirenderer {
namespace skiapipeline {
@@ -55,9 +57,7 @@
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 @@
*/
void renderVectorDrawableCache();
+ SkCanvas* tryCapture(SkSurface* surface);
+ void endCapture(SkSurface* surface);
+
std::vector<sk_sp<SkImage>> mPinnedImages;
/**
* populated by prepareTree with dirty VDs
*/
std::vector<VectorDrawableRoot*> 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<SavePictureProcessor> 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<SkPictureRecorder> 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 0000000..54fa229
--- /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 4c3e182..d182d78 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 @@
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 @@
LayerUpdateQueue layerUpdateQueue;
layerUpdateQueue.enqueueLayerWithDamage(child.get(),
android::uirenderer::Rect(LAYER_WIDTH, LAYER_HEIGHT));
- SkiaPipeline::renderLayersImpl(layerUpdateQueue, true, false);
+ auto pipeline = std::make_unique<SkiaOpenGLPipeline>(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);