diff options
-rw-r--r-- | core/java/android/view/RenderNode.java | 14 | ||||
-rw-r--r-- | core/java/android/view/ThreadedRenderer.java | 6 | ||||
-rw-r--r-- | core/java/android/view/View.java | 7 | ||||
-rw-r--r-- | core/java/android/view/ViewRootImpl.java | 2 | ||||
-rw-r--r-- | core/java/android/webkit/WebViewDelegate.java | 3 | ||||
-rw-r--r-- | core/jni/android_view_RenderNode.cpp | 55 | ||||
-rw-r--r-- | core/jni/android_view_ThreadedRenderer.cpp | 44 | ||||
-rw-r--r-- | libs/hwui/Android.mk | 1 | ||||
-rw-r--r-- | libs/hwui/RenderNode.cpp | 27 | ||||
-rw-r--r-- | libs/hwui/RenderNode.h | 24 | ||||
-rw-r--r-- | libs/hwui/TreeInfo.h | 16 | ||||
-rw-r--r-- | libs/hwui/renderthread/CanvasContext.cpp | 4 | ||||
-rw-r--r-- | libs/hwui/tests/common/TestUtils.h | 2 | ||||
-rw-r--r-- | libs/hwui/tests/unit/RenderNodeTests.cpp | 53 |
14 files changed, 225 insertions, 33 deletions
diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index a19254f2a6f6..4ffb3d3b64bf 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/android/view/RenderNode.java @@ -763,6 +763,16 @@ public class RenderNode { return nGetDebugSize(mNativeRenderNode); } + /** + * Called by native when the passed displaylist is removed from the draw tree + */ + void onRenderNodeDetached() { + discardDisplayList(); + if (mOwningView != null) { + mOwningView.onRenderNodeDetached(this); + } + } + /////////////////////////////////////////////////////////////////////////// // Animations /////////////////////////////////////////////////////////////////////////// @@ -795,7 +805,9 @@ public class RenderNode { // Native methods /////////////////////////////////////////////////////////////////////////// - private static native long nCreate(String name); + // Intentionally not static because it acquires a reference to 'this' + private native long nCreate(String name); + private static native void nDestroyRenderNode(long renderNode); private static native void nSetDisplayList(long renderNode, long newData); diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index c97247656540..df774b476192 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -794,7 +794,8 @@ public final class ThreadedRenderer { } final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo; - int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length); + int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length, + mRootNode.mNativeRenderNode); if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) { setEnabled(false); attachInfo.mViewRootImpl.mSurface.release(); @@ -993,7 +994,8 @@ public final class ThreadedRenderer { private static native void nSetLightCenter(long nativeProxy, float lightX, float lightY, float lightZ); private static native void nSetOpaque(long nativeProxy, boolean opaque); - private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size); + private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size, + long rootRenderNode); private static native void nDestroy(long nativeProxy, long rootRenderNode); private static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index dd79e62717b0..3ee6a8d1a179 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16009,6 +16009,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Called when the passed RenderNode is removed from the draw tree + * @hide + */ + public void onRenderNodeDetached(RenderNode renderNode) { + } + + /** * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p> * * @return A non-scaled bitmap representing this view or null if cache is disabled. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 5d4ee87432a6..420c4f272ff0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -774,7 +774,7 @@ public final class ViewRootImpl implements ViewParent, * has invoked. If false, the functor may be invoked * asynchronously. */ - public void invokeFunctor(long functor, boolean waitForCompletion) { + public static void invokeFunctor(long functor, boolean waitForCompletion) { ThreadedRenderer.invokeFunctor(functor, waitForCompletion); } diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 94dc03cad23b..8104f7d6f50c 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -86,8 +86,7 @@ public final class WebViewDelegate { */ public void invokeDrawGlFunctor(View containerView, long nativeDrawGLFunctor, boolean waitForCompletion) { - ViewRootImpl viewRootImpl = containerView.getViewRootImpl(); - viewRootImpl.invokeFunctor(nativeDrawGLFunctor, waitForCompletion); + ViewRootImpl.invokeFunctor(nativeDrawGLFunctor, waitForCompletion); } /** diff --git a/core/jni/android_view_RenderNode.cpp b/core/jni/android_view_RenderNode.cpp index 79b518fe53ee..27e2ee87f588 100644 --- a/core/jni/android_view_RenderNode.cpp +++ b/core/jni/android_view_RenderNode.cpp @@ -43,6 +43,54 @@ using namespace uirenderer; ? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \ : false) +static JNIEnv* getenv(JavaVM* vm) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", vm); + } + return env; +} + +static jmethodID gOnRenderNodeDetached; + +class RenderNodeContext : public VirtualLightRefBase { +public: + RenderNodeContext(JNIEnv* env, jobject jobjRef) { + env->GetJavaVM(&mVm); + // This holds a weak ref because otherwise there's a cyclic global ref + // with this holding a strong global ref to the view which holds + // a strong ref to RenderNode which holds a strong ref to this. + mWeakRef = env->NewWeakGlobalRef(jobjRef); + } + + virtual ~RenderNodeContext() { + JNIEnv* env = getenv(mVm); + env->DeleteWeakGlobalRef(mWeakRef); + } + + jobject acquireLocalRef(JNIEnv* env) { + return env->NewLocalRef(mWeakRef); + } + +private: + JavaVM* mVm; + jweak mWeakRef; +}; + +// Called by ThreadedRenderer's JNI layer +void onRenderNodeRemoved(JNIEnv* env, RenderNode* node) { + auto context = reinterpret_cast<RenderNodeContext*>(node->getUserContext()); + if (!context) return; + jobject jnode = context->acquireLocalRef(env); + if (!jnode) { + // The owning node has been GC'd, release the context + node->setUserContext(nullptr); + return; + } + env->CallVoidMethod(jnode, gOnRenderNodeDetached); + env->DeleteLocalRef(jnode); +} + // ---------------------------------------------------------------------------- // DisplayList view properties // ---------------------------------------------------------------------------- @@ -59,7 +107,8 @@ static jint android_view_RenderNode_getDebugSize(JNIEnv* env, return renderNode->getDebugSize(); } -static jlong android_view_RenderNode_create(JNIEnv* env, jobject clazz, jstring name) { +static jlong android_view_RenderNode_create(JNIEnv* env, jobject thiz, + jstring name) { RenderNode* renderNode = new RenderNode(); renderNode->incStrong(0); if (name != NULL) { @@ -67,6 +116,7 @@ static jlong android_view_RenderNode_create(JNIEnv* env, jobject clazz, jstring renderNode->setName(textArray); env->ReleaseStringUTFChars(name, textArray); } + renderNode->setUserContext(new RenderNodeContext(env, thiz)); return reinterpret_cast<jlong>(renderNode); } @@ -627,6 +677,9 @@ int register_android_view_RenderNode(JNIEnv* env) { jclass clazz = FindClassOrDie(env, "android/view/SurfaceView"); gSurfaceViewPositionUpdateMethod = GetMethodIDOrDie(env, clazz, "updateWindowPositionRT", "(JIIII)V"); + clazz = FindClassOrDie(env, "android/view/RenderNode"); + gOnRenderNodeDetached = GetMethodIDOrDie(env, clazz, + "onRenderNodeDetached", "()V"); return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); } diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp index 07868c54cf52..80193264bcf4 100644 --- a/core/jni/android_view_ThreadedRenderer.cpp +++ b/core/jni/android_view_ThreadedRenderer.cpp @@ -120,7 +120,13 @@ private: std::string mMessage; }; -class RootRenderNode : public RenderNode, ErrorHandler { +// TODO: Clean this up, it's a bit odd to need to call over to +// rendernode's jni layer. Probably means RootRenderNode should be pulled +// into HWUI with appropriate callbacks for the various JNI hooks so +// that RenderNode's JNI layer can handle its own thing +void onRenderNodeRemoved(JNIEnv* env, RenderNode* node); + +class RootRenderNode : public RenderNode, ErrorHandler, TreeObserver { public: RootRenderNode(JNIEnv* env) : RenderNode() { mLooper = Looper::getForThread(); @@ -131,12 +137,15 @@ public: virtual ~RootRenderNode() {} - virtual void onError(const std::string& message) { + virtual void onError(const std::string& message) override { mLooper->sendMessage(new RenderingException(mVm, message), 0); } - virtual void prepareTree(TreeInfo& info) { + virtual void prepareTree(TreeInfo& info) override { info.errorHandler = this; + if (info.mode == TreeInfo::MODE_FULL) { + info.observer = this; + } // TODO: This is hacky info.windowInsetLeft = -stagingProperties().getLeft(); info.windowInsetTop = -stagingProperties().getTop(); @@ -145,7 +154,8 @@ public: info.updateWindowPositions = false; info.windowInsetLeft = 0; info.windowInsetTop = 0; - info.errorHandler = NULL; + info.errorHandler = nullptr; + info.observer = nullptr; } void sendMessage(const sp<MessageHandler>& handler) { @@ -171,10 +181,27 @@ public: mPendingAnimatingRenderNodes.clear(); } + virtual void onMaybeRemovedFromTree(RenderNode* node) override { + mMaybeRemovedNodes.insert(sp<RenderNode>(node)); + } + + void processMaybeRemovedNodes(JNIEnv* env) { + // We can safely access mMaybeRemovedNodes here because + // we will only modify it in prepareTree calls that are + // MODE_FULL + + for (auto& node : mMaybeRemovedNodes) { + if (node->hasParents()) continue; + onRenderNodeRemoved(env, node.get()); + } + mMaybeRemovedNodes.clear(); + } + private: sp<Looper> mLooper; JavaVM* mVm; std::vector< sp<RenderNode> > mPendingAnimatingRenderNodes; + std::set< sp<RenderNode> > mMaybeRemovedNodes; }; class AnimationContextBridge : public AnimationContext { @@ -473,13 +500,16 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz, } static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, - jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) { + jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize, jlong rootNodePtr) { LOG_ALWAYS_FATAL_IF(frameInfoSize != UI_THREAD_FRAME_INFO_SIZE, "Mismatched size expectations, given %d expected %d", frameInfoSize, UI_THREAD_FRAME_INFO_SIZE); RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); + RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootNodePtr); env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo()); - return proxy->syncAndDrawFrame(); + int ret = proxy->syncAndDrawFrame(); + rootRenderNode->processMaybeRemovedNodes(env); + return ret; } static void android_view_ThreadedRenderer_destroy(JNIEnv* env, jobject clazz, @@ -706,7 +736,7 @@ static const JNINativeMethod gMethods[] = { { "nSetup", "(JIIFII)V", (void*) android_view_ThreadedRenderer_setup }, { "nSetLightCenter", "(JFFF)V", (void*) android_view_ThreadedRenderer_setLightCenter }, { "nSetOpaque", "(JZ)V", (void*) android_view_ThreadedRenderer_setOpaque }, - { "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame }, + { "nSyncAndDrawFrame", "(J[JIJ)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame }, { "nDestroy", "(JJ)V", (void*) android_view_ThreadedRenderer_destroy }, { "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode }, { "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor }, diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index 1f1785139257..be816f78e4ff 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -252,6 +252,7 @@ LOCAL_SRC_FILES += \ tests/unit/LinearAllocatorTests.cpp \ tests/unit/MatrixTests.cpp \ tests/unit/OffscreenBufferPoolTests.cpp \ + tests/unit/RenderNodeTests.cpp \ tests/unit/SkiaBehaviorTests.cpp \ tests/unit/StringUtilsTests.cpp \ tests/unit/TextDropShadowCacheTests.cpp \ diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 61441ce9b16e..f6f92f731c1c 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -68,7 +68,7 @@ RenderNode::RenderNode() } RenderNode::~RenderNode() { - deleteDisplayList(); + deleteDisplayList(nullptr); delete mStagingDisplayList; #if HWUI_NEW_OPS LOG_ALWAYS_FATAL_IF(mLayer, "layer missed detachment!"); @@ -88,7 +88,7 @@ void RenderNode::setStagingDisplayList(DisplayList* displayList) { // If mParentCount == 0 we are the sole reference to this RenderNode, // so immediately free the old display list if (!mParentCount && !mStagingDisplayList) { - deleteDisplayList(); + deleteDisplayList(nullptr); } } @@ -462,7 +462,7 @@ void RenderNode::applyLayerPropertiesToLayer(TreeInfo& info) { } #endif -void RenderNode::syncDisplayList() { +void RenderNode::syncDisplayList(TreeObserver* observer) { // Make sure we inc first so that we don't fluctuate between 0 and 1, // which would thrash the layer cache if (mStagingDisplayList) { @@ -470,7 +470,7 @@ void RenderNode::syncDisplayList() { child->renderNode->incParentRefCount(); } } - deleteDisplayList(); + deleteDisplayList(observer); mDisplayList = mStagingDisplayList; mStagingDisplayList = nullptr; if (mDisplayList) { @@ -486,15 +486,15 @@ void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) { // Damage with the old display list first then the new one to catch any // changes in isRenderable or, in the future, bounds damageSelf(info); - syncDisplayList(); + syncDisplayList(info.observer); damageSelf(info); } } -void RenderNode::deleteDisplayList() { +void RenderNode::deleteDisplayList(TreeObserver* observer) { if (mDisplayList) { for (auto&& child : mDisplayList->getChildren()) { - child->renderNode->decParentRefCount(); + child->renderNode->decParentRefCount(observer); } } delete mDisplayList; @@ -526,32 +526,35 @@ void RenderNode::prepareSubTree(TreeInfo& info, bool functorsNeedLayer, DisplayL } } -void RenderNode::destroyHardwareResources() { +void RenderNode::destroyHardwareResources(TreeObserver* observer) { if (mLayer) { destroyLayer(mLayer); mLayer = nullptr; } if (mDisplayList) { for (auto&& child : mDisplayList->getChildren()) { - child->renderNode->destroyHardwareResources(); + child->renderNode->destroyHardwareResources(observer); } if (mNeedsDisplayListSync) { // Next prepare tree we are going to push a new display list, so we can // drop our current one now - deleteDisplayList(); + deleteDisplayList(observer); } } } -void RenderNode::decParentRefCount() { +void RenderNode::decParentRefCount(TreeObserver* observer) { LOG_ALWAYS_FATAL_IF(!mParentCount, "already 0!"); mParentCount--; if (!mParentCount) { + if (observer) { + observer->onMaybeRemovedFromTree(this); + } // If a child of ours is being attached to our parent then this will incorrectly // destroy its hardware resources. However, this situation is highly unlikely // and the failure is "just" that the layer is re-created, so this should // be safe enough - destroyHardwareResources(); + destroyHardwareResources(observer); } } diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index 838192552127..b0136cfb35bb 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -68,6 +68,7 @@ class SaveLayerOp; class SaveOp; class RestoreToCountOp; class TreeInfo; +class TreeObserver; namespace proto { class RenderNode; @@ -154,6 +155,14 @@ public: } } + VirtualLightRefBase* getUserContext() const { + return mUserContext.get(); + } + + void setUserContext(VirtualLightRefBase* context) { + mUserContext = context; + } + bool isPropertyFieldDirty(DirtyPropertyMask field) const { return mDirtyPropertyFields & field; } @@ -187,7 +196,7 @@ public: } ANDROID_API virtual void prepareTree(TreeInfo& info); - void destroyHardwareResources(); + void destroyHardwareResources(TreeObserver* observer); // UI thread only! ANDROID_API void addAnimator(const sp<BaseRenderNodeAnimator>& animator); @@ -232,6 +241,12 @@ public: mPositionListener.reset(listener); } + // This is only modified in MODE_FULL, so it can be safely accessed + // on the UI thread. + ANDROID_API bool hasParents() { + return mParentCount; + } + private: typedef key_value_pair_t<float, DrawRenderNodeOp*> ZDrawRenderNodeOpPair; @@ -291,7 +306,7 @@ private: void syncProperties(); - void syncDisplayList(); + void syncDisplayList(TreeObserver* observer); void prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer); void pushStagingPropertiesChanges(TreeInfo& info); @@ -302,13 +317,14 @@ private: #endif void prepareLayer(TreeInfo& info, uint32_t dirtyMask); void pushLayerUpdate(TreeInfo& info); - void deleteDisplayList(); + void deleteDisplayList(TreeObserver* observer); void damageSelf(TreeInfo& info); void incParentRefCount() { mParentCount++; } - void decParentRefCount(); + void decParentRefCount(TreeObserver* observer); String8 mName; + sp<VirtualLightRefBase> mUserContext; uint32_t mDirtyPropertyFields; RenderProperties mProperties; diff --git a/libs/hwui/TreeInfo.h b/libs/hwui/TreeInfo.h index accd3038cb9c..a43e544b4507 100644 --- a/libs/hwui/TreeInfo.h +++ b/libs/hwui/TreeInfo.h @@ -32,6 +32,7 @@ class CanvasContext; class DamageAccumulator; class LayerUpdateQueue; class OpenGLRenderer; +class RenderNode; class RenderState; class ErrorHandler { @@ -41,6 +42,17 @@ protected: ~ErrorHandler() {} }; +class TreeObserver { +public: + // Called when a RenderNode's parent count hits 0. + // Due to the unordered nature of tree pushes, once prepareTree + // is finished it is possible that the node was "resurrected" and has + // a non-zero parent count. + virtual void onMaybeRemovedFromTree(RenderNode* node) {} +protected: + ~TreeObserver() {} +}; + // This would be a struct, but we want to PREVENT_COPY_AND_ASSIGN class TreeInfo { PREVENT_COPY_AND_ASSIGN(TreeInfo); @@ -86,6 +98,10 @@ public: #endif ErrorHandler* errorHandler = nullptr; + // Optional, may be nullptr. Used to allow things to observe interesting + // tree state changes + TreeObserver* observer = nullptr; + // Frame number for use with synchronized surfaceview position updating int64_t frameNumber = -1; int32_t windowInsetLeft = 0; diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 6933b2f1e23c..63fa788ba251 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -576,7 +576,7 @@ void CanvasContext::markLayerInUse(RenderNode* node) { static void destroyPrefetechedNode(RenderNode* node) { ALOGW("Incorrectly called buildLayer on View: %s, destroying layer...", node->getName()); - node->destroyHardwareResources(); + node->destroyHardwareResources(nullptr); node->decStrong(nullptr); } @@ -641,7 +641,7 @@ void CanvasContext::destroyHardwareResources() { if (mEglManager.hasEglContext()) { freePrefetechedLayers(); for (const sp<RenderNode>& node : mRenderNodes) { - node->destroyHardwareResources(); + node->destroyHardwareResources(nullptr); } Caches& caches = Caches::getInstance(); // Make sure to release all the textures we were owning as there won't diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 2d1e2e9db225..5492035fd0da 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -218,7 +218,7 @@ public: private: static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) { node->syncProperties(); - node->syncDisplayList(); + node->syncDisplayList(nullptr); auto displayList = node->getDisplayList(); if (displayList) { for (auto&& childOp : displayList->getChildren()) { diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp new file mode 100644 index 000000000000..7c57a50c951d --- /dev/null +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> + +#include "RenderNode.h" +#include "TreeInfo.h" +#include "tests/common/TestUtils.h" +#include "utils/Color.h" + +using namespace android; +using namespace android::uirenderer; + +TEST(RenderNode, hasParents) { + auto child = TestUtils::createNode(0, 0, 200, 400, + [](RenderProperties& props, TestCanvas& canvas) { + canvas.drawColor(Color::Red_500, SkXfermode::kSrcOver_Mode); + }); + auto parent = TestUtils::createNode(0, 0, 200, 400, + [&child](RenderProperties& props, TestCanvas& canvas) { + canvas.drawRenderNode(child.get()); + }); + + TestUtils::syncHierarchyPropertiesAndDisplayList(parent); + + EXPECT_TRUE(child->hasParents()) << "Child node has no parent"; + EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents"; + + TestUtils::recordNode(*parent, [](TestCanvas& canvas) { + canvas.drawColor(Color::Amber_500, SkXfermode::kSrcOver_Mode); + }); + + EXPECT_TRUE(child->hasParents()) << "Child should still have a parent"; + EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents"; + + TestUtils::syncHierarchyPropertiesAndDisplayList(parent); + + EXPECT_FALSE(child->hasParents()) << "Child should be removed"; + EXPECT_FALSE(parent->hasParents()) << "Root node shouldn't have any parents"; +} |