From 5cca8f250cd287d311b9e7b560a6c10fb909c6bc Mon Sep 17 00:00:00 2001 From: John Reck Date: Mon, 10 Dec 2018 17:06:22 -0800 Subject: [PATCH] Add continuous SKP capture test api Bug: 122856066 Test: PictureCaptureDemo Change-Id: Iaf3a4bc1c8a2c18c7dff635c5f1cf726b331f8bf --- api/current.txt | 6 +- api/removed.txt | 4 +- api/test-current.txt | 4 + core/java/android/view/ThreadedRenderer.java | 5 + core/java/android/view/ViewDebug.java | 125 ++++++++++++++ core/jni/android/graphics/Picture.cpp | 6 + core/jni/android/graphics/Picture.h | 1 + core/jni/android_view_ThreadedRenderer.cpp | 36 +++++ .../android/graphics/HardwareRenderer.java | 26 +++ graphics/java/android/graphics/Picture.java | 52 ++++-- .../java/android/graphics/RenderNode.java | 2 +- libs/hwui/VectorDrawable.cpp | 13 ++ libs/hwui/pipeline/skia/GLFunctorDrawable.cpp | 8 +- libs/hwui/pipeline/skia/SkiaPipeline.cpp | 64 +++++--- libs/hwui/pipeline/skia/SkiaPipeline.h | 7 + libs/hwui/renderthread/CanvasContext.h | 4 + libs/hwui/renderthread/IRenderPipeline.h | 10 +- libs/hwui/renderthread/RenderProxy.cpp | 32 ++-- libs/hwui/renderthread/RenderProxy.h | 2 + tests/HwAccelerationTest/Android.mk | 1 + tests/HwAccelerationTest/AndroidManifest.xml | 9 ++ .../android/test/hwui/PictureCaptureDemo.java | 153 ++++++++++++++++++ 22 files changed, 508 insertions(+), 62 deletions(-) create mode 100644 tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java diff --git a/api/current.txt b/api/current.txt index 28dd9ecb2294c..8f165d8305ac0 100644 --- a/api/current.txt +++ b/api/current.txt @@ -14618,8 +14618,8 @@ package android.graphics { public class Picture { ctor public Picture(); ctor public Picture(android.graphics.Picture); - method public android.graphics.Canvas beginRecording(int, int); - method public void draw(android.graphics.Canvas); + method @NonNull public android.graphics.Canvas beginRecording(int, int); + method public void draw(@NonNull android.graphics.Canvas); method public void endRecording(); method public int getHeight(); method public int getWidth(); @@ -14871,7 +14871,7 @@ package android.graphics { } public final class RenderNode { - ctor public RenderNode(String); + ctor public RenderNode(@Nullable String); method public int computeApproximateMemoryUsage(); method public void discardDisplayList(); method public void endRecording(); diff --git a/api/removed.txt b/api/removed.txt index e23222719ea90..9f4b0416246d8 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -239,8 +239,8 @@ package android.graphics { } public class Picture { - method @Deprecated public static android.graphics.Picture createFromStream(java.io.InputStream); - method @Deprecated public void writeToStream(java.io.OutputStream); + method @Deprecated public static android.graphics.Picture createFromStream(@NonNull java.io.InputStream); + method @Deprecated public void writeToStream(@NonNull java.io.OutputStream); } @Deprecated public class PixelXorXfermode extends android.graphics.Xfermode { diff --git a/api/test-current.txt b/api/test-current.txt index d08983103ce86..ba778408eb856 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2268,6 +2268,10 @@ package android.view { method public static int getLongPressTooltipHideTimeout(); } + public class ViewDebug { + method @Nullable public static AutoCloseable startRenderingCommandsCapture(android.view.View, java.util.concurrent.Executor, java.util.function.Function); + } + public interface WindowManager extends android.view.ViewManager { method public default void setShouldShowIme(int, boolean); method public default void setShouldShowSystemDecors(int, boolean); diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 34d076fba54d3..47b206ca0dcaa 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.content.res.TypedArray; import android.graphics.HardwareRenderer; +import android.graphics.Picture; import android.graphics.Point; import android.graphics.RecordingCanvas; import android.graphics.Rect; @@ -553,6 +554,10 @@ public final class ThreadedRenderer extends HardwareRenderer { dumpProfileInfo(fd, flags); } + Picture captureRenderingCommands() { + return null; + } + @Override public boolean loadSystemProperties() { boolean changed = super.loadSystemProperties(); diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 292e933c3f7e3..5afc07f35ba00 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -17,17 +17,21 @@ package android.view; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.HardwareRenderer; import android.graphics.Picture; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RenderNode; import android.os.Debug; import android.os.Handler; +import android.os.Looper; import android.os.RemoteException; import android.util.DisplayMetrics; import android.util.Log; @@ -48,16 +52,20 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; /** * Various debugging/tracing tools related to {@link View} and the view hierarchy. @@ -741,6 +749,123 @@ public class ViewDebug { root.getViewRootImpl().outputDisplayList(target); } + private static class PictureCallbackHandler implements AutoCloseable, + HardwareRenderer.PictureCapturedCallback, Runnable { + private final HardwareRenderer mRenderer; + private final Function mCallback; + private final Executor mExecutor; + private final ReentrantLock mLock = new ReentrantLock(false); + private final ArrayDeque mQueue = new ArrayDeque<>(3); + private boolean mStopListening; + private Thread mRenderThread; + + private PictureCallbackHandler(HardwareRenderer renderer, + Function callback, Executor executor) { + mRenderer = renderer; + mCallback = callback; + mExecutor = executor; + mRenderer.setPictureCaptureCallback(this); + } + + @Override + public void close() { + mLock.lock(); + mStopListening = true; + mLock.unlock(); + mRenderer.setPictureCaptureCallback(null); + } + + @Override + public void onPictureCaptured(Picture picture) { + mLock.lock(); + if (mStopListening) { + mLock.unlock(); + mRenderer.setPictureCaptureCallback(null); + return; + } + if (mRenderThread == null) { + mRenderThread = Thread.currentThread(); + } + Picture toDestroy = null; + if (mQueue.size() == 3) { + toDestroy = mQueue.removeLast(); + } + mQueue.add(picture); + mLock.unlock(); + if (toDestroy == null) { + mExecutor.execute(this); + } else { + toDestroy.close(); + } + } + + @Override + public void run() { + mLock.lock(); + final Picture picture = mQueue.poll(); + final boolean isStopped = mStopListening; + mLock.unlock(); + if (Thread.currentThread() == mRenderThread) { + close(); + throw new IllegalStateException( + "ViewDebug#startRenderingCommandsCapture must be given an executor that " + + "invokes asynchronously"); + } + if (isStopped) { + picture.close(); + return; + } + final boolean keepReceiving = mCallback.apply(picture); + if (!keepReceiving) { + close(); + } + } + } + + /** + * Begins capturing the entire rendering commands for the view tree referenced by the given + * view. The view passed may be any View in the tree as long as it is attached. That is, + * {@link View#isAttachedToWindow()} must be true. + * + * Every time a frame is rendered a Picture will be passed to the given callback via the given + * executor. As long as the callback returns 'true' it will continue to receive new frames. + * The system will only invoke the callback at a rate that the callback is able to keep up with. + * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running + * then the callback will only receive 33% of the frames produced. + * + * This method must be called on the same thread as the View tree. + * + * @param tree The View tree to capture the rendering commands. + * @param callback The callback to invoke on every frame produced. Should return true to + * continue receiving new frames, false to stop capturing. + * @param executor The executor to invoke the callback on. Recommend using a background thread + * to avoid stalling the UI thread. Must be an asynchronous invoke or an + * exception will be thrown. + * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note + * that the callback may continue to receive another frame or two depending on thread timings. + * Returns null if the capture stream cannot be started, such as if there's no + * HardwareRenderer for the given view tree. + * @hide + */ + @TestApi + @Nullable + public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor, + Function callback) { + final View.AttachInfo attachInfo = tree.mAttachInfo; + if (attachInfo == null) { + throw new IllegalArgumentException("Given view isn't attached"); + } + if (attachInfo.mHandler.getLooper() != Looper.myLooper()) { + throw new IllegalStateException("Called on the wrong thread." + + " Must be called on the thread that owns the given View"); + } + final HardwareRenderer renderer = attachInfo.mThreadedRenderer; + if (renderer != null) { + return new PictureCallbackHandler(renderer, callback, executor); + } + return null; + } + private static void capture(View root, final OutputStream clientStream, String parameter) throws IOException { diff --git a/core/jni/android/graphics/Picture.cpp b/core/jni/android/graphics/Picture.cpp index fd1d87ff62f6b..d29857d0cf128 100644 --- a/core/jni/android/graphics/Picture.cpp +++ b/core/jni/android/graphics/Picture.cpp @@ -37,6 +37,12 @@ Picture::Picture(const Picture* src) { } } +Picture::Picture(sk_sp&& src) { + mPicture = std::move(src); + mWidth = 0; + mHeight = 0; +} + Canvas* Picture::beginRecording(int width, int height) { mPicture.reset(NULL); mRecorder.reset(new SkPictureRecorder); diff --git a/core/jni/android/graphics/Picture.h b/core/jni/android/graphics/Picture.h index 3068631743349..536f651473a93 100644 --- a/core/jni/android/graphics/Picture.h +++ b/core/jni/android/graphics/Picture.h @@ -37,6 +37,7 @@ class Canvas; class Picture { public: explicit Picture(const Picture* src = NULL); + explicit Picture(sk_sp&& src); Canvas* beginRecording(int width, int height); diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp index 5a8ab3c1bdc4a..318ec9b2ff0dd 100644 --- a/core/jni/android_view_ThreadedRenderer.cpp +++ b/core/jni/android_view_ThreadedRenderer.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,11 @@ struct { jmethodID callback; } gFrameMetricsObserverClassInfo; +struct { + jclass clazz; + jmethodID invokePictureCapturedCallback; +} gHardwareRenderer; + struct { jmethodID onFrameDraw; } gFrameDrawingCallback; @@ -905,6 +911,27 @@ private: jobject mObject; }; +static void android_view_ThreadedRenderer_setPictureCapturedCallbackJNI(JNIEnv* env, + jobject clazz, jlong proxyPtr, jobject pictureCallback) { + RenderProxy* proxy = reinterpret_cast(proxyPtr); + if (!pictureCallback) { + proxy->setPictureCapturedCallback(nullptr); + } else { + JavaVM* vm = nullptr; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&vm) != JNI_OK, "Unable to get Java VM"); + auto globalCallbackRef = std::make_shared(vm, + env->NewGlobalRef(pictureCallback)); + proxy->setPictureCapturedCallback([globalCallbackRef](sk_sp&& picture) { + JNIEnv* env = getenv(globalCallbackRef->vm()); + Picture* wrapper = new Picture{std::move(picture)}; + env->CallStaticVoidMethod(gHardwareRenderer.clazz, + gHardwareRenderer.invokePictureCapturedCallback, + static_cast(reinterpret_cast(wrapper)), + globalCallbackRef->object()); + }); + } +} + static void android_view_ThreadedRenderer_setFrameCallback(JNIEnv* env, jobject clazz, jlong proxyPtr, jobject frameCallback) { RenderProxy* proxy = reinterpret_cast(proxyPtr); @@ -1145,6 +1172,8 @@ static const JNINativeMethod gMethods[] = { { "nRemoveRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_removeRenderNode}, { "nDrawRenderNode", "(JJ)V", (void*) android_view_ThreadedRendererd_drawRenderNode}, { "nSetContentDrawBounds", "(JIIII)V", (void*)android_view_ThreadedRenderer_setContentDrawBounds}, + { "nSetPictureCaptureCallback", "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V", + (void*) android_view_ThreadedRenderer_setPictureCapturedCallbackJNI }, { "nSetFrameCallback", "(JLandroid/graphics/HardwareRenderer$FrameDrawingCallback;)V", (void*)android_view_ThreadedRenderer_setFrameCallback}, { "nSetFrameCompleteCallback", "(JLandroid/graphics/HardwareRenderer$FrameCompleteCallback;)V", @@ -1198,6 +1227,13 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) { gFrameMetricsObserverClassInfo.timingDataBuffer = GetFieldIDOrDie( env, metricsClass, "mTimingData", "[J"); + jclass hardwareRenderer = FindClassOrDie(env, + "android/graphics/HardwareRenderer"); + gHardwareRenderer.clazz = reinterpret_cast(env->NewGlobalRef(hardwareRenderer)); + gHardwareRenderer.invokePictureCapturedCallback = GetStaticMethodIDOrDie(env, hardwareRenderer, + "invokePictureCapturedCallback", + "(JLandroid/graphics/HardwareRenderer$PictureCapturedCallback;)V"); + jclass frameCallbackClass = FindClassOrDie(env, "android/graphics/HardwareRenderer$FrameDrawingCallback"); gFrameDrawingCallback.onFrameDraw = GetMethodIDOrDie(env, frameCallbackClass, diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index e4020554786b0..c4ddd50616e88 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -20,6 +20,7 @@ import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.Activity; import android.app.ActivityManager; import android.os.IBinder; @@ -667,6 +668,17 @@ public class HardwareRenderer { nSetContentDrawBounds(mNativeProxy, left, top, right, bottom); } + /** @hide */ + public void setPictureCaptureCallback(@Nullable PictureCapturedCallback callback) { + nSetPictureCaptureCallback(mNativeProxy, callback); + } + + /** called by native */ + static void invokePictureCapturedCallback(long picturePtr, PictureCapturedCallback callback) { + Picture picture = new Picture(picturePtr); + callback.onPictureCaptured(picture); + } + /** * Interface used to receive callbacks when a frame is being drawn. * @@ -695,6 +707,17 @@ public class HardwareRenderer { void onFrameComplete(long frameNr); } + /** + * Interface for listening to picture captures + * @hide + */ + @TestApi + public interface PictureCapturedCallback { + /** @hide */ + @TestApi + void onPictureCaptured(Picture picture); + } + private static void validateAlpha(float alpha, String argumentName) { if (!(alpha >= 0.0f && alpha <= 1.0f)) { throw new IllegalArgumentException(argumentName + " must be a valid alpha, " @@ -998,6 +1021,9 @@ public class HardwareRenderer { private static native void nSetContentDrawBounds(long nativeProxy, int left, int top, int right, int bottom); + private static native void nSetPictureCaptureCallback(long nativeProxy, + PictureCapturedCallback callback); + private static native void nSetFrameCallback(long nativeProxy, FrameDrawingCallback callback); private static native void nSetFrameCompleteCallback(long nativeProxy, diff --git a/graphics/java/android/graphics/Picture.java b/graphics/java/android/graphics/Picture.java index f6d801b3ba43f..8d12cbffc793f 100644 --- a/graphics/java/android/graphics/Picture.java +++ b/graphics/java/android/graphics/Picture.java @@ -16,6 +16,7 @@ package android.graphics; +import android.annotation.NonNull; import android.annotation.UnsupportedAppUsage; import java.io.InputStream; @@ -34,7 +35,8 @@ import java.io.OutputStream; */ public class Picture { private PictureCanvas mRecordingCanvas; - @UnsupportedAppUsage + // TODO: Figure out if this was a false-positive + @UnsupportedAppUsage(maxTargetSdk = 28) private long mNativePicture; private boolean mRequiresHwAcceleration; @@ -56,23 +58,43 @@ public class Picture { this(nativeConstructor(src != null ? src.mNativePicture : 0)); } - private Picture(long nativePicture) { + /** @hide */ + public Picture(long nativePicture) { if (nativePicture == 0) { - throw new RuntimeException(); + throw new IllegalArgumentException(); } mNativePicture = nativePicture; } + /** + * Immediately releases the backing data of the Picture. This object will no longer + * be usable after calling this, and any further calls on the Picture will throw an + * IllegalStateException. + * // TODO: Support? + * @hide + */ + public void close() { + if (mNativePicture != 0) { + nativeDestructor(mNativePicture); + mNativePicture = 0; + } + } + @Override protected void finalize() throws Throwable { try { - nativeDestructor(mNativePicture); - mNativePicture = 0; + close(); } finally { super.finalize(); } } + private void verifyValid() { + if (mNativePicture == 0) { + throw new IllegalStateException("Picture is destroyed"); + } + } + /** * To record a picture, call beginRecording() and then draw into the Canvas * that is returned. Nothing we appear on screen, but all of the draw @@ -81,7 +103,9 @@ public class Picture { * that was returned must no longer be used, and nothing should be drawn * into it. */ + @NonNull public Canvas beginRecording(int width, int height) { + verifyValid(); if (mRecordingCanvas != null) { throw new IllegalStateException("Picture already recording, must call #endRecording()"); } @@ -98,6 +122,7 @@ public class Picture { * or {@link Canvas#drawPicture(Picture)} is called. */ public void endRecording() { + verifyValid(); if (mRecordingCanvas != null) { mRequiresHwAcceleration = mRecordingCanvas.mHoldsHwBitmap; mRecordingCanvas = null; @@ -110,7 +135,8 @@ public class Picture { * does not reflect (per se) the content of the picture. */ public int getWidth() { - return nativeGetWidth(mNativePicture); + verifyValid(); + return nativeGetWidth(mNativePicture); } /** @@ -118,7 +144,8 @@ public class Picture { * does not reflect (per se) the content of the picture. */ public int getHeight() { - return nativeGetHeight(mNativePicture); + verifyValid(); + return nativeGetHeight(mNativePicture); } /** @@ -133,6 +160,7 @@ public class Picture { * false otherwise. */ public boolean requiresHardwareAcceleration() { + verifyValid(); return mRequiresHwAcceleration; } @@ -149,7 +177,8 @@ public class Picture { * * @param canvas The picture is drawn to this canvas */ - public void draw(Canvas canvas) { + public void draw(@NonNull Canvas canvas) { + verifyValid(); if (mRecordingCanvas != null) { endRecording(); } @@ -172,7 +201,7 @@ public class Picture { * raw or compressed pixels. */ @Deprecated - public static Picture createFromStream(InputStream stream) { + public static Picture createFromStream(@NonNull InputStream stream) { return new Picture(nativeCreateFromStream(stream, new byte[WORKING_STREAM_STORAGE])); } @@ -188,10 +217,11 @@ public class Picture { * Bitmap from which you can persist it as raw or compressed pixels. */ @Deprecated - public void writeToStream(OutputStream stream) { + public void writeToStream(@NonNull OutputStream stream) { + verifyValid(); // do explicit check before calling the native method if (stream == null) { - throw new NullPointerException(); + throw new IllegalArgumentException("stream cannot be null"); } if (!nativeWriteToStream(mNativePicture, stream, new byte[WORKING_STREAM_STORAGE])) { throw new RuntimeException(); diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index 3b1d44b44ed4e..09b18b7712500 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -184,7 +184,7 @@ public final class RenderNode { * * @param name The name of the RenderNode, used for debugging purpose. May be null. */ - public RenderNode(String name) { + public RenderNode(@Nullable String name) { this(name, null); } diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index dd62bbbdc84f7..72656922b03ef 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -551,6 +551,19 @@ void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) SkPaint paint = inPaint; paint.setAlpha(mProperties.getRootAlpha() * 255); + if (canvas->getGrContext() == nullptr) { + // Recording to picture, don't use the SkSurface which won't work off of renderthread. + Bitmap& bitmap = getBitmapUpdateIfDirty(); + SkBitmap skiaBitmap; + bitmap.getSkBitmap(&skiaBitmap); + + int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth()); + int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight()); + canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds, + &paint, SkCanvas::kFast_SrcRectConstraint); + return; + } + SkRect src; sk_sp vdSurface = mCache.getSurface(&src); if (vdSurface) { diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index 240efb41285ca..60c8057410582 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -74,7 +74,13 @@ static bool GetFboDetails(SkCanvas* canvas, GLuint* outFboID, SkISize* outFboSiz void GLFunctorDrawable::onDraw(SkCanvas* canvas) { if (canvas->getGrContext() == nullptr) { - SkDEBUGF(("Attempting to draw GLFunctor into an unsupported surface")); + // We're dumping a picture, render a light-blue rectangle instead + // TODO: Draw the WebView text on top? Seemingly complicated as SkPaint doesn't + // seem to have a default typeface that works. We only ever use drawGlyphs, which + // requires going through minikin & hwui's canvas which we don't have here. + SkPaint paint; + paint.setColor(0xFF81D4FA); + canvas->drawRect(mBounds, paint); return; } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index df8224372ea78..47c90948bbbea 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -111,7 +111,7 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) const Rect& layerDamage = layers.entries()[i].damage; - SkCanvas* layerCanvas = tryCapture(layerNode->getLayerSurface()); + SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); int saveCount = layerCanvas->save(); SkASSERT(saveCount == 1); @@ -139,8 +139,6 @@ void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) 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 = layerNode->getLayerSurface()->getCanvas()->getGrContext(); @@ -244,6 +242,7 @@ public: } virtual void onProcess(const sp>& task) override { + ATRACE_NAME("SavePictureTask"); SavePictureTask* t = static_cast(task.get()); if (0 == access(t->filename.c_str(), F_OK)) { @@ -265,46 +264,56 @@ public: SkCanvas* SkiaPipeline::tryCapture(SkSurface* surface) { if (CC_UNLIKELY(Properties::skpCaptureEnabled)) { - bool recordingPicture = mCaptureSequence > 0; char prop[PROPERTY_VALUE_MAX] = {'\0'}; - if (!recordingPicture) { + if (mCaptureSequence <= 0) { property_get(PROPERTY_CAPTURE_SKP_FILENAME, prop, "0"); - recordingPicture = prop[0] != '0' && - mCapturedFile != prop; // ensure we capture only once per filename - if (recordingPicture) { + if (prop[0] != '0' && mCapturedFile != prop) { mCapturedFile = prop; mCaptureSequence = property_get_int32(PROPERTY_CAPTURE_SKP_FRAMES, 1); } } - if (recordingPicture) { + if (mCaptureSequence > 0 || mPictureCapturedCallback) { mRecorder.reset(new SkPictureRecorder()); - return mRecorder->beginRecording(surface->width(), surface->height(), nullptr, - SkPictureRecorder::kPlaybackDrawPicture_RecordFlag); + 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 surface->getCanvas(); } void SkiaPipeline::endCapture(SkSurface* surface) { + mNwayCanvas.reset(); if (CC_UNLIKELY(mRecorder.get())) { + ATRACE_CALL(); sk_sp picture = mRecorder->finishRecordingAsPicture(); - surface->getCanvas()->drawPicture(picture); if (picture->approximateOpCount() > 0) { - auto data = picture->serialize(); + if (mCaptureSequence > 0) { + ATRACE_BEGIN("picture->serialize"); + auto data = picture->serialize(); + ATRACE_END(); - // offload saving to file in a different thread - if (!mSavePictureProcessor.get()) { - TaskManager* taskManager = getTaskManager(); - mSavePictureProcessor = new SavePictureProcessor( - taskManager->canRunTasks() ? taskManager : nullptr); + // 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(data, mCapturedFile); + } else { + mSavePictureProcessor->savePicture( + data, + mCapturedFile + "_" + std::to_string(mCaptureSequence)); + } + mCaptureSequence--; } - if (1 == mCaptureSequence) { - mSavePictureProcessor->savePicture(data, mCapturedFile); - } else { - mSavePictureProcessor->savePicture( - data, mCapturedFile + "_" + std::to_string(mCaptureSequence)); + if (mPictureCapturedCallback) { + std::invoke(mPictureCapturedCallback, std::move(picture)); } - mCaptureSequence--; } mRecorder.reset(); } @@ -314,6 +323,11 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli const std::vector>& nodes, bool opaque, const Rect& contentDrawBounds, sk_sp surface, const SkMatrix& preTransform) { + bool previousSkpEnabled = Properties::skpCaptureEnabled; + if (mPictureCapturedCallback) { + Properties::skpCaptureEnabled = true; + } + renderVectorDrawableCache(); // draw all layers up front @@ -334,6 +348,8 @@ void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& cli ATRACE_NAME("flush commands"); surface->getCanvas()->flush(); + + Properties::skpCaptureEnabled = previousSkpEnabled; } namespace { diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index cf6f5b284d8cf..e9957df95f104 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -105,6 +105,11 @@ public: mLightCenter = lightGeometry.center; } + void setPictureCapturedCallback( + const std::function&&)>& callback) override { + mPictureCapturedCallback = callback; + } + protected: void dumpResourceCacheUsage() const; void setSurfaceColorProperties(renderthread::ColorMode colorMode); @@ -163,6 +168,8 @@ private: * parallel tryCapture calls (not really needed). */ std::unique_ptr mRecorder; + std::unique_ptr mNwayCanvas; + std::function&&)> mPictureCapturedCallback; static float mLightRadius; static uint8_t mAmbientShadowAlpha; diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 9e7abf447cd68..db97763acd8bc 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -184,6 +184,10 @@ public: mFrameCompleteCallbacks.push_back(std::move(func)); } + void setPictureCapturedCallback(const std::function&&)>& callback) { + mRenderPipeline->setPictureCapturedCallback(callback); + } + void setForceDark(bool enable) { mUseForceDark = enable; } diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index d4dd62941440b..2cfc8df382970 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -59,15 +59,15 @@ public: virtual MakeCurrentResult makeCurrent() = 0; virtual Frame getFrame() = 0; virtual bool draw(const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, - const LightGeometry& lightGeometry, - LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, - bool opaque, const LightInfo& lightInfo, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector>& renderNodes, FrameInfoVisualizer* profiler) = 0; virtual bool swapBuffers(const Frame& frame, bool drew, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) = 0; virtual DeferredLayerUpdater* createTextureLayer() = 0; - virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior, ColorMode colorMode) = 0; + virtual bool setSurface(ANativeWindow* window, SwapBehavior swapBehavior, + ColorMode colorMode) = 0; virtual void onStop() = 0; virtual bool isSurfaceReady() = 0; virtual bool isContextReady() = 0; @@ -85,6 +85,8 @@ public: virtual SkColorType getSurfaceColorType() const = 0; virtual sk_sp getSurfaceColorSpace() = 0; virtual GrSurfaceOrigin getSurfaceOrigin() = 0; + virtual void setPictureCapturedCallback( + const std::function&&)>& callback) = 0; virtual ~IRenderPipeline() {} }; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index aa6af23d8ed33..ab59af71d3443 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -21,6 +21,7 @@ #include "Properties.h" #include "Readback.h" #include "Rect.h" +#include "WebViewFunctorManager.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" #include "pipeline/skia/VectorDrawableAtlas.h" #include "renderstate/RenderState.h" @@ -30,7 +31,6 @@ #include "renderthread/RenderThread.h" #include "utils/Macros.h" #include "utils/TimeUtils.h" -#include "WebViewFunctorManager.h" #include @@ -147,9 +147,7 @@ void RenderProxy::invokeFunctor(Functor* functor, bool waitForCompletion) { void RenderProxy::destroyFunctor(int functor) { ATRACE_CALL(); RenderThread& thread = RenderThread::getInstance(); - thread.queue().post([=]() { - WebViewFunctorManager::instance().destroyFunctor(functor); - }); + thread.queue().post([=]() { WebViewFunctorManager::instance().destroyFunctor(functor); }); } DeferredLayerUpdater* RenderProxy::createTextureLayer() { @@ -164,9 +162,9 @@ void RenderProxy::buildLayer(RenderNode* node) { bool RenderProxy::copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap) { auto& thread = RenderThread::getInstance(); - return thread.queue().runSync( - [&]() -> bool { return thread.readback().copyLayerInto(layer, &bitmap) - == CopyResult::Success; }); + return thread.queue().runSync([&]() -> bool { + return thread.readback().copyLayerInto(layer, &bitmap) == CopyResult::Success; + }); } void RenderProxy::pushLayerUpdate(DeferredLayerUpdater* layer) { @@ -204,9 +202,8 @@ void RenderProxy::fence() { } int RenderProxy::maxTextureSize() { - static int maxTextureSize = RenderThread::getInstance().queue().runSync([]() { - return DeviceInfo::get()->maxTextureSize(); - }); + static int maxTextureSize = RenderThread::getInstance().queue().runSync( + []() { return DeviceInfo::get()->maxTextureSize(); }); return maxTextureSize; } @@ -281,6 +278,12 @@ void RenderProxy::setContentDrawBounds(int left, int top, int right, int bottom) mDrawFrameTask.setContentDrawBounds(left, top, right, bottom); } +void RenderProxy::setPictureCapturedCallback( + const std::function&&)>& callback) { + mRenderThread.queue().post( + [ this, cb = callback ]() { mContext->setPictureCapturedCallback(cb); }); +} + void RenderProxy::setFrameCallback(std::function&& callback) { mDrawFrameTask.setFrameCallback(std::move(callback)); } @@ -302,9 +305,7 @@ void RenderProxy::removeFrameMetricsObserver(FrameMetricsObserver* observerPtr) } void RenderProxy::setForceDark(bool enable) { - mRenderThread.queue().post([this, enable]() { - mContext->setForceDark(enable); - }); + mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); }); } int RenderProxy::copySurfaceInto(sp& surface, int left, int top, int right, int bottom, @@ -348,9 +349,8 @@ int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); } else { - return thread.queue().runSync([&]() -> int { - return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); - }); + return thread.queue().runSync( + [&]() -> int { return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); }); } } diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 9dc918121be69..6e1bfd74528a7 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -114,6 +114,8 @@ public: ANDROID_API void removeRenderNode(RenderNode* node); ANDROID_API void drawRenderNode(RenderNode* node); ANDROID_API void setContentDrawBounds(int left, int top, int right, int bottom); + ANDROID_API void setPictureCapturedCallback( + const std::function&&)>& callback); ANDROID_API void setFrameCallback(std::function&& callback); ANDROID_API void setFrameCompleteCallback(std::function&& callback); diff --git a/tests/HwAccelerationTest/Android.mk b/tests/HwAccelerationTest/Android.mk index 11ea954c62c7a..79072faeaa0ea 100644 --- a/tests/HwAccelerationTest/Android.mk +++ b/tests/HwAccelerationTest/Android.mk @@ -21,6 +21,7 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_PACKAGE_NAME := HwAccelerationTest LOCAL_PRIVATE_PLATFORM_APIS := true +LOCAL_CERTIFICATE := platform LOCAL_MODULE_TAGS := tests diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml index c8f96c9f0670a..f330b8353b500 100644 --- a/tests/HwAccelerationTest/AndroidManifest.xml +++ b/tests/HwAccelerationTest/AndroidManifest.xml @@ -310,6 +310,15 @@ + + + + + + + { + if (rand.nextInt(20) == 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + Canvas canvas = holder.lockCanvas(); + if (canvas == null) { + return false; + } + canvas.drawPicture(picture); + holder.unlockCanvasAndPost(canvas); + picture.close(); + return true; + }); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (mStopCapture != null) { + try { + mStopCapture.close(); + } catch (Exception e) { + } + mStopCapture = null; + } + } + }); + } + + ExecutorService mCaptureThread = Executors.newSingleThreadExecutor(); + ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + Picture deepCopy(Picture src) { + try { + PipedInputStream inputStream = new PipedInputStream(); + PipedOutputStream outputStream = new PipedOutputStream(inputStream); + Future future = mExecutor.submit(() -> Picture.createFromStream(inputStream)); + src.writeToStream(outputStream); + outputStream.close(); + return future.get(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +}