RenderNode.cpp revision 56ad6ec42f814e9e61030ff819cac4e5d31def8b
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17#include "RenderNode.h" 18 19#include "BakedOpRenderer.h" 20#include "DamageAccumulator.h" 21#include "Debug.h" 22#include "OpDumper.h" 23#include "RecordedOp.h" 24#include "TreeInfo.h" 25#include "utils/MathUtils.h" 26#include "utils/TraceUtils.h" 27#include "VectorDrawable.h" 28#include "renderstate/RenderState.h" 29#include "renderthread/CanvasContext.h" 30 31#include "protos/hwui.pb.h" 32#include "protos/ProtoHelpers.h" 33 34#include <algorithm> 35#include <sstream> 36#include <string> 37 38namespace android { 39namespace uirenderer { 40 41RenderNode::RenderNode() 42 : mDirtyPropertyFields(0) 43 , mNeedsDisplayListSync(false) 44 , mDisplayList(nullptr) 45 , mStagingDisplayList(nullptr) 46 , mAnimatorManager(*this) 47 , mParentCount(0) { 48} 49 50RenderNode::~RenderNode() { 51 deleteDisplayList(nullptr); 52 delete mStagingDisplayList; 53 LOG_ALWAYS_FATAL_IF(mLayer, "layer missed detachment!"); 54} 55 56void RenderNode::setStagingDisplayList(DisplayList* displayList, TreeObserver* observer) { 57 mNeedsDisplayListSync = true; 58 delete mStagingDisplayList; 59 mStagingDisplayList = displayList; 60 // If mParentCount == 0 we are the sole reference to this RenderNode, 61 // so immediately free the old display list 62 if (!mParentCount && !mStagingDisplayList) { 63 deleteDisplayList(observer); 64 } 65} 66 67/** 68 * This function is a simplified version of replay(), where we simply retrieve and log the 69 * display list. This function should remain in sync with the replay() function. 70 */ 71void RenderNode::output(uint32_t level, const char* label) { 72 ALOGD("%s (%s %p%s%s%s%s%s)", 73 label, 74 getName(), 75 this, 76 (MathUtils::isZero(properties().getAlpha()) ? ", zero alpha" : ""), 77 (properties().hasShadow() ? ", casting shadow" : ""), 78 (isRenderable() ? "" : ", empty"), 79 (properties().getProjectBackwards() ? ", projected" : ""), 80 (mLayer != nullptr ? ", on HW Layer" : "")); 81 properties().debugOutputProperties(level + 1); 82 83 if (mDisplayList) { 84 for (auto&& op : mDisplayList->getOps()) { 85 std::stringstream strout; 86 OpDumper::dump(*op, strout, level + 1); 87 if (op->opId == RecordedOpId::RenderNodeOp) { 88 auto rnOp = reinterpret_cast<const RenderNodeOp*>(op); 89 rnOp->renderNode->output(level + 1, strout.str().c_str()); 90 } else { 91 ALOGD("%s", strout.str().c_str()); 92 } 93 } 94 } 95 ALOGD("%*s/RenderNode(%s %p)", level * 2, "", getName(), this); 96} 97 98void RenderNode::copyTo(proto::RenderNode *pnode) { 99 pnode->set_id(static_cast<uint64_t>( 100 reinterpret_cast<uintptr_t>(this))); 101 pnode->set_name(mName.string(), mName.length()); 102 103 proto::RenderProperties* pprops = pnode->mutable_properties(); 104 pprops->set_left(properties().getLeft()); 105 pprops->set_top(properties().getTop()); 106 pprops->set_right(properties().getRight()); 107 pprops->set_bottom(properties().getBottom()); 108 pprops->set_clip_flags(properties().getClippingFlags()); 109 pprops->set_alpha(properties().getAlpha()); 110 pprops->set_translation_x(properties().getTranslationX()); 111 pprops->set_translation_y(properties().getTranslationY()); 112 pprops->set_translation_z(properties().getTranslationZ()); 113 pprops->set_elevation(properties().getElevation()); 114 pprops->set_rotation(properties().getRotation()); 115 pprops->set_rotation_x(properties().getRotationX()); 116 pprops->set_rotation_y(properties().getRotationY()); 117 pprops->set_scale_x(properties().getScaleX()); 118 pprops->set_scale_y(properties().getScaleY()); 119 pprops->set_pivot_x(properties().getPivotX()); 120 pprops->set_pivot_y(properties().getPivotY()); 121 pprops->set_has_overlapping_rendering(properties().getHasOverlappingRendering()); 122 pprops->set_pivot_explicitly_set(properties().isPivotExplicitlySet()); 123 pprops->set_project_backwards(properties().getProjectBackwards()); 124 pprops->set_projection_receiver(properties().isProjectionReceiver()); 125 set(pprops->mutable_clip_bounds(), properties().getClipBounds()); 126 127 const Outline& outline = properties().getOutline(); 128 if (outline.getType() != Outline::Type::None) { 129 proto::Outline* poutline = pprops->mutable_outline(); 130 poutline->clear_path(); 131 if (outline.getType() == Outline::Type::Empty) { 132 poutline->set_type(proto::Outline_Type_Empty); 133 } else if (outline.getType() == Outline::Type::ConvexPath) { 134 poutline->set_type(proto::Outline_Type_ConvexPath); 135 if (const SkPath* path = outline.getPath()) { 136 set(poutline->mutable_path(), *path); 137 } 138 } else if (outline.getType() == Outline::Type::RoundRect) { 139 poutline->set_type(proto::Outline_Type_RoundRect); 140 } else { 141 ALOGW("Uknown outline type! %d", static_cast<int>(outline.getType())); 142 poutline->set_type(proto::Outline_Type_None); 143 } 144 poutline->set_should_clip(outline.getShouldClip()); 145 poutline->set_alpha(outline.getAlpha()); 146 poutline->set_radius(outline.getRadius()); 147 set(poutline->mutable_bounds(), outline.getBounds()); 148 } else { 149 pprops->clear_outline(); 150 } 151 152 const RevealClip& revealClip = properties().getRevealClip(); 153 if (revealClip.willClip()) { 154 proto::RevealClip* prevealClip = pprops->mutable_reveal_clip(); 155 prevealClip->set_x(revealClip.getX()); 156 prevealClip->set_y(revealClip.getY()); 157 prevealClip->set_radius(revealClip.getRadius()); 158 } else { 159 pprops->clear_reveal_clip(); 160 } 161 162 pnode->clear_children(); 163 if (mDisplayList) { 164 for (auto&& child : mDisplayList->getChildren()) { 165 child->renderNode->copyTo(pnode->add_children()); 166 } 167 } 168} 169 170int RenderNode::getDebugSize() { 171 int size = sizeof(RenderNode); 172 if (mStagingDisplayList) { 173 size += mStagingDisplayList->getUsedSize(); 174 } 175 if (mDisplayList && mDisplayList != mStagingDisplayList) { 176 size += mDisplayList->getUsedSize(); 177 } 178 return size; 179} 180 181void RenderNode::prepareTree(TreeInfo& info) { 182 ATRACE_CALL(); 183 LOG_ALWAYS_FATAL_IF(!info.damageAccumulator, "DamageAccumulator missing"); 184 185 // Functors don't correctly handle stencil usage of overdraw debugging - shove 'em in a layer. 186 bool functorsNeedLayer = Properties::debugOverdraw; 187 188 prepareTreeImpl(info, functorsNeedLayer); 189} 190 191void RenderNode::addAnimator(const sp<BaseRenderNodeAnimator>& animator) { 192 mAnimatorManager.addAnimator(animator); 193} 194 195void RenderNode::removeAnimator(const sp<BaseRenderNodeAnimator>& animator) { 196 mAnimatorManager.removeAnimator(animator); 197} 198 199void RenderNode::damageSelf(TreeInfo& info) { 200 if (isRenderable()) { 201 if (properties().getClipDamageToBounds()) { 202 info.damageAccumulator->dirty(0, 0, properties().getWidth(), properties().getHeight()); 203 } else { 204 // Hope this is big enough? 205 // TODO: Get this from the display list ops or something 206 info.damageAccumulator->dirty(DIRTY_MIN, DIRTY_MIN, DIRTY_MAX, DIRTY_MAX); 207 } 208 } 209} 210 211void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) { 212 LayerType layerType = properties().effectiveLayerType(); 213 if (CC_UNLIKELY(layerType == LayerType::RenderLayer)) { 214 // Damage applied so far needs to affect our parent, but does not require 215 // the layer to be updated. So we pop/push here to clear out the current 216 // damage and get a clean state for display list or children updates to 217 // affect, which will require the layer to be updated 218 info.damageAccumulator->popTransform(); 219 info.damageAccumulator->pushTransform(this); 220 if (dirtyMask & DISPLAY_LIST) { 221 damageSelf(info); 222 } 223 } 224} 225 226static OffscreenBuffer* createLayer(RenderState& renderState, uint32_t width, uint32_t height) { 227 return renderState.layerPool().get(renderState, width, height); 228} 229 230static void destroyLayer(OffscreenBuffer* layer) { 231 RenderState& renderState = layer->renderState; 232 renderState.layerPool().putOrDelete(layer); 233} 234 235static bool layerMatchesWidthAndHeight(OffscreenBuffer* layer, int width, int height) { 236 return layer->viewportWidth == (uint32_t) width && layer->viewportHeight == (uint32_t)height; 237} 238 239void RenderNode::pushLayerUpdate(TreeInfo& info) { 240 LayerType layerType = properties().effectiveLayerType(); 241 // If we are not a layer OR we cannot be rendered (eg, view was detached) 242 // we need to destroy any Layers we may have had previously 243 if (CC_LIKELY(layerType != LayerType::RenderLayer) 244 || CC_UNLIKELY(!isRenderable()) 245 || CC_UNLIKELY(properties().getWidth() == 0) 246 || CC_UNLIKELY(properties().getHeight() == 0)) { 247 if (CC_UNLIKELY(mLayer)) { 248 destroyLayer(mLayer); 249 mLayer = nullptr; 250 } 251 return; 252 } 253 254 bool transformUpdateNeeded = false; 255 if (!mLayer) { 256 mLayer = createLayer(info.canvasContext.getRenderState(), getWidth(), getHeight()); 257 damageSelf(info); 258 transformUpdateNeeded = true; 259 } else if (!layerMatchesWidthAndHeight(mLayer, getWidth(), getHeight())) { 260 // TODO: remove now irrelevant, currently enqueued damage (respecting damage ordering) 261 // Or, ideally, maintain damage between frames on node/layer so ordering is always correct 262 RenderState& renderState = mLayer->renderState; 263 if (properties().fitsOnLayer()) { 264 mLayer = renderState.layerPool().resize(mLayer, getWidth(), getHeight()); 265 } else { 266 destroyLayer(mLayer); 267 mLayer = nullptr; 268 } 269 damageSelf(info); 270 transformUpdateNeeded = true; 271 } 272 273 SkRect dirty; 274 info.damageAccumulator->peekAtDirty(&dirty); 275 276 if (!mLayer) { 277 Caches::getInstance().dumpMemoryUsage(); 278 if (info.errorHandler) { 279 std::ostringstream err; 280 err << "Unable to create layer for " << getName(); 281 const int maxTextureSize = Caches::getInstance().maxTextureSize; 282 if (getWidth() > maxTextureSize || getHeight() > maxTextureSize) { 283 err << ", size " << getWidth() << "x" << getHeight() 284 << " exceeds max size " << maxTextureSize; 285 } else { 286 err << ", see logcat for more info"; 287 } 288 info.errorHandler->onError(err.str()); 289 } 290 return; 291 } 292 293 if (transformUpdateNeeded && mLayer) { 294 // update the transform in window of the layer to reset its origin wrt light source position 295 Matrix4 windowTransform; 296 info.damageAccumulator->computeCurrentTransform(&windowTransform); 297 mLayer->setWindowTransform(windowTransform); 298 } 299 300 info.layerUpdateQueue->enqueueLayerWithDamage(this, dirty); 301 302 // There might be prefetched layers that need to be accounted for. 303 // That might be us, so tell CanvasContext that this layer is in the 304 // tree and should not be destroyed. 305 info.canvasContext.markLayerInUse(this); 306} 307 308/** 309 * Traverse down the the draw tree to prepare for a frame. 310 * 311 * MODE_FULL = UI Thread-driven (thus properties must be synced), otherwise RT driven 312 * 313 * While traversing down the tree, functorsNeedLayer flag is set to true if anything that uses the 314 * stencil buffer may be needed. Views that use a functor to draw will be forced onto a layer. 315 */ 316void RenderNode::prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer) { 317 info.damageAccumulator->pushTransform(this); 318 319 if (info.mode == TreeInfo::MODE_FULL) { 320 pushStagingPropertiesChanges(info); 321 } 322 uint32_t animatorDirtyMask = 0; 323 if (CC_LIKELY(info.runAnimations)) { 324 animatorDirtyMask = mAnimatorManager.animate(info); 325 } 326 327 bool willHaveFunctor = false; 328 if (info.mode == TreeInfo::MODE_FULL && mStagingDisplayList) { 329 willHaveFunctor = !mStagingDisplayList->getFunctors().empty(); 330 } else if (mDisplayList) { 331 willHaveFunctor = !mDisplayList->getFunctors().empty(); 332 } 333 bool childFunctorsNeedLayer = mProperties.prepareForFunctorPresence( 334 willHaveFunctor, functorsNeedLayer); 335 336 if (CC_UNLIKELY(mPositionListener.get())) { 337 mPositionListener->onPositionUpdated(*this, info); 338 } 339 340 prepareLayer(info, animatorDirtyMask); 341 if (info.mode == TreeInfo::MODE_FULL) { 342 pushStagingDisplayListChanges(info); 343 } 344 prepareSubTree(info, childFunctorsNeedLayer, mDisplayList); 345 pushLayerUpdate(info); 346 347 if (mDisplayList) { 348 for (auto& vectorDrawable : mDisplayList->getVectorDrawables()) { 349 // If any vector drawable in the display list needs update, damage the node. 350 if (vectorDrawable->isDirty()) { 351 damageSelf(info); 352 } 353 vectorDrawable->setPropertyChangeWillBeConsumed(true); 354 } 355 } 356 357 info.damageAccumulator->popTransform(); 358} 359 360void RenderNode::syncProperties() { 361 mProperties = mStagingProperties; 362} 363 364void RenderNode::pushStagingPropertiesChanges(TreeInfo& info) { 365 // Push the animators first so that setupStartValueIfNecessary() is called 366 // before properties() is trampled by stagingProperties(), as they are 367 // required by some animators. 368 if (CC_LIKELY(info.runAnimations)) { 369 mAnimatorManager.pushStaging(); 370 } 371 if (mDirtyPropertyFields) { 372 mDirtyPropertyFields = 0; 373 damageSelf(info); 374 info.damageAccumulator->popTransform(); 375 syncProperties(); 376 // We could try to be clever and only re-damage if the matrix changed. 377 // However, we don't need to worry about that. The cost of over-damaging 378 // here is only going to be a single additional map rect of this node 379 // plus a rect join(). The parent's transform (and up) will only be 380 // performed once. 381 info.damageAccumulator->pushTransform(this); 382 damageSelf(info); 383 } 384} 385 386void RenderNode::syncDisplayList(TreeInfo* info) { 387 // Make sure we inc first so that we don't fluctuate between 0 and 1, 388 // which would thrash the layer cache 389 if (mStagingDisplayList) { 390 for (auto&& child : mStagingDisplayList->getChildren()) { 391 child->renderNode->incParentRefCount(); 392 } 393 } 394 deleteDisplayList(info ? info->observer : nullptr, info); 395 mDisplayList = mStagingDisplayList; 396 mStagingDisplayList = nullptr; 397 if (mDisplayList) { 398 for (auto& iter : mDisplayList->getFunctors()) { 399 (*iter.functor)(DrawGlInfo::kModeSync, nullptr); 400 } 401 for (auto& vectorDrawable : mDisplayList->getVectorDrawables()) { 402 vectorDrawable->syncProperties(); 403 } 404 } 405} 406 407void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) { 408 if (mNeedsDisplayListSync) { 409 mNeedsDisplayListSync = false; 410 // Damage with the old display list first then the new one to catch any 411 // changes in isRenderable or, in the future, bounds 412 damageSelf(info); 413 syncDisplayList(&info); 414 damageSelf(info); 415 } 416} 417 418void RenderNode::deleteDisplayList(TreeObserver* observer, TreeInfo* info) { 419 if (mDisplayList) { 420 for (auto&& child : mDisplayList->getChildren()) { 421 child->renderNode->decParentRefCount(observer, info); 422 } 423 } 424 delete mDisplayList; 425 mDisplayList = nullptr; 426} 427 428void RenderNode::prepareSubTree(TreeInfo& info, bool functorsNeedLayer, DisplayList* subtree) { 429 if (subtree) { 430 TextureCache& cache = Caches::getInstance().textureCache; 431 info.out.hasFunctors |= subtree->getFunctors().size(); 432 for (auto&& bitmapResource : subtree->getBitmapResources()) { 433 void* ownerToken = &info.canvasContext; 434 info.prepareTextures = cache.prefetchAndMarkInUse(ownerToken, bitmapResource); 435 } 436 for (auto&& op : subtree->getChildren()) { 437 RenderNode* childNode = op->renderNode; 438 info.damageAccumulator->pushTransform(&op->localMatrix); 439 bool childFunctorsNeedLayer = functorsNeedLayer; // TODO! || op->mRecordedWithPotentialStencilClip; 440 childNode->prepareTreeImpl(info, childFunctorsNeedLayer); 441 info.damageAccumulator->popTransform(); 442 } 443 } 444} 445 446void RenderNode::destroyHardwareResources(TreeObserver* observer, TreeInfo* info) { 447 if (mLayer) { 448 destroyLayer(mLayer); 449 mLayer = nullptr; 450 } 451 if (mDisplayList) { 452 for (auto&& child : mDisplayList->getChildren()) { 453 child->renderNode->destroyHardwareResources(observer, info); 454 } 455 if (mNeedsDisplayListSync) { 456 // Next prepare tree we are going to push a new display list, so we can 457 // drop our current one now 458 deleteDisplayList(observer, info); 459 } 460 } 461} 462 463void RenderNode::decParentRefCount(TreeObserver* observer, TreeInfo* info) { 464 LOG_ALWAYS_FATAL_IF(!mParentCount, "already 0!"); 465 mParentCount--; 466 if (!mParentCount) { 467 if (observer) { 468 observer->onMaybeRemovedFromTree(this); 469 } 470 if (CC_UNLIKELY(mPositionListener.get())) { 471 mPositionListener->onPositionLost(*this, info); 472 } 473 // If a child of ours is being attached to our parent then this will incorrectly 474 // destroy its hardware resources. However, this situation is highly unlikely 475 // and the failure is "just" that the layer is re-created, so this should 476 // be safe enough 477 destroyHardwareResources(observer, info); 478 } 479} 480 481/** 482 * Apply property-based transformations to input matrix 483 * 484 * If true3dTransform is set to true, the transform applied to the input matrix will use true 4x4 485 * matrix computation instead of the Skia 3x3 matrix + camera hackery. 486 */ 487void RenderNode::applyViewPropertyTransforms(mat4& matrix, bool true3dTransform) const { 488 if (properties().getLeft() != 0 || properties().getTop() != 0) { 489 matrix.translate(properties().getLeft(), properties().getTop()); 490 } 491 if (properties().getStaticMatrix()) { 492 mat4 stat(*properties().getStaticMatrix()); 493 matrix.multiply(stat); 494 } else if (properties().getAnimationMatrix()) { 495 mat4 anim(*properties().getAnimationMatrix()); 496 matrix.multiply(anim); 497 } 498 499 bool applyTranslationZ = true3dTransform && !MathUtils::isZero(properties().getZ()); 500 if (properties().hasTransformMatrix() || applyTranslationZ) { 501 if (properties().isTransformTranslateOnly()) { 502 matrix.translate(properties().getTranslationX(), properties().getTranslationY(), 503 true3dTransform ? properties().getZ() : 0.0f); 504 } else { 505 if (!true3dTransform) { 506 matrix.multiply(*properties().getTransformMatrix()); 507 } else { 508 mat4 true3dMat; 509 true3dMat.loadTranslate( 510 properties().getPivotX() + properties().getTranslationX(), 511 properties().getPivotY() + properties().getTranslationY(), 512 properties().getZ()); 513 true3dMat.rotate(properties().getRotationX(), 1, 0, 0); 514 true3dMat.rotate(properties().getRotationY(), 0, 1, 0); 515 true3dMat.rotate(properties().getRotation(), 0, 0, 1); 516 true3dMat.scale(properties().getScaleX(), properties().getScaleY(), 1); 517 true3dMat.translate(-properties().getPivotX(), -properties().getPivotY()); 518 519 matrix.multiply(true3dMat); 520 } 521 } 522 } 523} 524 525/** 526 * Organizes the DisplayList hierarchy to prepare for background projection reordering. 527 * 528 * This should be called before a call to defer() or drawDisplayList() 529 * 530 * Each DisplayList that serves as a 3d root builds its list of composited children, 531 * which are flagged to not draw in the standard draw loop. 532 */ 533void RenderNode::computeOrdering() { 534 ATRACE_CALL(); 535 mProjectedNodes.clear(); 536 537 // TODO: create temporary DDLOp and call computeOrderingImpl on top DisplayList so that 538 // transform properties are applied correctly to top level children 539 if (mDisplayList == nullptr) return; 540 for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) { 541 RenderNodeOp* childOp = mDisplayList->getChildren()[i]; 542 childOp->renderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity()); 543 } 544} 545 546void RenderNode::computeOrderingImpl( 547 RenderNodeOp* opState, 548 std::vector<RenderNodeOp*>* compositedChildrenOfProjectionSurface, 549 const mat4* transformFromProjectionSurface) { 550 mProjectedNodes.clear(); 551 if (mDisplayList == nullptr || mDisplayList->isEmpty()) return; 552 553 // TODO: should avoid this calculation in most cases 554 // TODO: just calculate single matrix, down to all leaf composited elements 555 Matrix4 localTransformFromProjectionSurface(*transformFromProjectionSurface); 556 localTransformFromProjectionSurface.multiply(opState->localMatrix); 557 558 if (properties().getProjectBackwards()) { 559 // composited projectee, flag for out of order draw, save matrix, and store in proj surface 560 opState->skipInOrderDraw = true; 561 opState->transformFromCompositingAncestor = localTransformFromProjectionSurface; 562 compositedChildrenOfProjectionSurface->push_back(opState); 563 } else { 564 // standard in order draw 565 opState->skipInOrderDraw = false; 566 } 567 568 if (mDisplayList->getChildren().size() > 0) { 569 const bool isProjectionReceiver = mDisplayList->projectionReceiveIndex >= 0; 570 bool haveAppliedPropertiesToProjection = false; 571 for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) { 572 RenderNodeOp* childOp = mDisplayList->getChildren()[i]; 573 RenderNode* child = childOp->renderNode; 574 575 std::vector<RenderNodeOp*>* projectionChildren = nullptr; 576 const mat4* projectionTransform = nullptr; 577 if (isProjectionReceiver && !child->properties().getProjectBackwards()) { 578 // if receiving projections, collect projecting descendant 579 580 // Note that if a direct descendant is projecting backwards, we pass its 581 // grandparent projection collection, since it shouldn't project onto its 582 // parent, where it will already be drawing. 583 projectionChildren = &mProjectedNodes; 584 projectionTransform = &mat4::identity(); 585 } else { 586 if (!haveAppliedPropertiesToProjection) { 587 applyViewPropertyTransforms(localTransformFromProjectionSurface); 588 haveAppliedPropertiesToProjection = true; 589 } 590 projectionChildren = compositedChildrenOfProjectionSurface; 591 projectionTransform = &localTransformFromProjectionSurface; 592 } 593 child->computeOrderingImpl(childOp, projectionChildren, projectionTransform); 594 } 595 } 596} 597 598} /* namespace uirenderer */ 599} /* namespace android */ 600