Android上Path的硬件加速渲染流程分析

在上一篇文章让Android TextureView Canvas支持硬件加速中,介绍了如何在TextureView中获取支持硬件加速的绘制Canvas,然而后续发现虽然Canvas支持硬件加速了,但测试结果却表明在渲染复杂Path的场景中,相比于软件渲染,虽然渲染线程单帧渲染指令的执行耗时有明显降低(从10ms降低至1ms),但在CPU消耗上虽然有一定的降低,但不及预期(在Pixel 7上从13%降低至8%),分析后发现切换至硬件加速渲染后,RenderThread上的渲染任务执行耗时明显增加,因此详细跟踪了下在Android 13上Path的硬件加速渲染流程。

场景测试

基于Pixel 7(Android 13)

只绘制背景色

绘制逻辑:

kotlin 复制代码
canvas.drawColor(Color.WHITE)

RenderThread执行细节:

单次DrawFrames耗时2ms以内

绘制背景色 + 50 stroked path

绘制逻辑:

kotlin 复制代码
canvas.drawColor(Color.WHITE)
pathList.forEach { path ->  
    canvas.drawPath(path.path, paint)  
}

绘制内容:

RenderThread执行细节:

DrawFrames 耗时17.6ms,Drawing 耗时17.5ms,flush commands 耗时1.1ms,queueBuffer耗时0.2ms

流程分析

RenderThread的渲染任务是在DrawFrameTask中实现的,这也是trace上DrawFrames的入口:

DrawFrameTask.cpp

cpp 复制代码
void DrawFrameTask::run() {
    const int64_t vsyncId = mFrameInfo[static_cast<int>(FrameInfoIndex::FrameTimelineVsyncId)];
    ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId);

    mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued);
    mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio);

    auto hardwareBufferParams = mHardwareBufferParams;
    mContext->setHardwareBufferRenderParams(hardwareBufferParams);
    IRenderPipeline* pipeline = mContext->getRenderPipeline();
    bool canUnblockUiThread;
    bool canDrawThisFrame;
    bool solelyTextureViewUpdates;
    {
        TreeInfo info(TreeInfo::MODE_FULL, *mContext);
        info.forceDrawFrame = mForceDrawFrame;
        mForceDrawFrame = false;
        canUnblockUiThread = syncFrameState(info);
        canDrawThisFrame = info.out.canDrawThisFrame;
        solelyTextureViewUpdates = info.out.solelyTextureViewUpdates;

        if (mFrameCommitCallback) {
            mContext->addFrameCommitListener(std::move(mFrameCommitCallback));
            mFrameCommitCallback = nullptr;
        }
    }

    // Grab a copy of everything we need
    CanvasContext* context = mContext;
    std::function<std::function<void(bool)>(int32_t, int64_t)> frameCallback =
            std::move(mFrameCallback);
    std::function<void()> frameCompleteCallback = std::move(mFrameCompleteCallback);
    mFrameCallback = nullptr;
    mFrameCompleteCallback = nullptr;

    // From this point on anything in "this" is *UNSAFE TO ACCESS*
    if (canUnblockUiThread) {
        unblockUiThread();
    }

    // Even if we aren't drawing this vsync pulse the next frame number will still be accurate
    if (CC_UNLIKELY(frameCallback)) {
        context->enqueueFrameWork([frameCallback, context, syncResult = mSyncResult,
                                   frameNr = context->getFrameNumber()]() {
            auto frameCommitCallback = frameCallback(syncResult, frameNr);
            if (frameCommitCallback) {
                context->addFrameCommitListener(std::move(frameCommitCallback));
            }
        });
    }

    if (CC_LIKELY(canDrawThisFrame)) {
        context->draw(solelyTextureViewUpdates);
    } else {
        // Do a flush in case syncFrameState performed any texture uploads. Since we skipped
        // the draw() call, those uploads (or deletes) will end up sitting in the queue.
        // Do them now
        if (GrDirectContext* grContext = mRenderThread->getGrContext()) {
            grContext->flushAndSubmit();
        }
        // wait on fences so tasks don't overlap next frame
        context->waitOnFences();
    }

    if (CC_UNLIKELY(frameCompleteCallback)) {
        std::invoke(frameCompleteCallback);
    }

    if (!canUnblockUiThread) {
        unblockUiThread();
    }

    if (pipeline->hasHardwareBuffer()) {
        auto fence = pipeline->flush();
        hardwareBufferParams.invokeRenderCallback(std::move(fence), 0);
    }
}

这里调用了CanvasContext.draw(bool)实现渲染任务,也是trace上Drawing的入口,同时也是主要耗时点:

CanvasContext.cpp

cpp 复制代码
void CanvasContext::draw(bool solelyTextureViewUpdates) {
    if (auto grContext = getGrContext()) {
        if (grContext->abandoned()) {
            LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw");
            return;
        }
    }
    SkRect dirty;
    mDamageAccumulator.finish(&dirty);

    // reset syncDelayDuration each time we draw
    nsecs_t syncDelayDuration = mSyncDelayDuration;
    nsecs_t idleDuration = mIdleDuration;
    mSyncDelayDuration = 0;
    mIdleDuration = 0;

    if (!Properties::isDrawingEnabled() ||
        (dirty.isEmpty() && Properties::skipEmptyFrames && !surfaceRequiresRedraw())) {
        mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
        if (auto grContext = getGrContext()) {
            // Submit to ensure that any texture uploads complete and Skia can
            // free its staging buffers.
            grContext->flushAndSubmit();
        }

        // Notify the callbacks, even if there's nothing to draw so they aren't waiting
        // indefinitely
        waitOnFences();
        for (auto& func : mFrameCommitCallbacks) {
            std::invoke(func, false /* didProduceBuffer */);
        }
        mFrameCommitCallbacks.clear();
        return;
    }

    ScopedActiveContext activeContext(this);
    mCurrentFrameInfo->set(FrameInfoIndex::FrameInterval) =
            mRenderThread.timeLord().frameIntervalNanos();

    mCurrentFrameInfo->markIssueDrawCommandsStart();

    Frame frame = getFrame();

    SkRect windowDirty = computeDirtyRect(frame, &dirty);

    ATRACE_FORMAT("Drawing " RECT_STRING, SK_RECT_ARGS(dirty));

    IRenderPipeline::DrawResult drawResult;
    {
        // FrameInfoVisualizer accesses the frame events, which cannot be mutated mid-draw
        // or it can lead to memory corruption.
        // This lock is overly broad, but it's the quickest fix since this mutex is otherwise
        // not visible to IRenderPipeline much less FrameInfoVisualizer. And since this is
        // the thread we're primarily concerned about being responsive, this being too broad
        // shouldn't pose a performance issue.
        std::scoped_lock lock(mFrameMetricsReporterMutex);
        drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry,
                                           &mLayerUpdateQueue, mContentDrawBounds, mOpaque,
                                           mLightInfo, mRenderNodes, &(profiler()), mBufferParams);
    }

    uint64_t frameCompleteNr = getFrameNumber();

    waitOnFences();

    if (mNativeSurface) {
        // TODO(b/165985262): measure performance impact
        const auto vsyncId = mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId);
        if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) {
            const auto inputEventId =
                    static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId));
            const ANativeWindowFrameTimelineInfo ftl = {
                    .frameNumber = frameCompleteNr,
                    .frameTimelineVsyncId = vsyncId,
                    .inputEventId = inputEventId,
                    .startTimeNanos = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime),
                    .useForRefreshRateSelection = solelyTextureViewUpdates,
                    .skippedFrameVsyncId = mSkippedFrameInfo ? mSkippedFrameInfo->vsyncId
                                                             : UiFrameInfoBuilder::INVALID_VSYNC_ID,
                    .skippedFrameStartTimeNanos =
                            mSkippedFrameInfo ? mSkippedFrameInfo->startTime : 0,
            };
            native_window_set_frame_timeline_info(mNativeSurface->getNativeWindow(), ftl);
        }
    }

    bool requireSwap = false;
    bool didDraw = false;

    int error = OK;
    bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty,
                                                mCurrentFrameInfo, &requireSwap);

    mCurrentFrameInfo->set(FrameInfoIndex::CommandSubmissionCompleted) = std::max(
            drawResult.commandSubmissionTime, mCurrentFrameInfo->get(FrameInfoIndex::SwapBuffers));
    // ...
}

这里继续向下调用了IRenderPipeline.draw方法,在Pixel 7(Android 13)上硬件加速默认是由Vulkan 实现的,在其他机器上可能是OpenGL实现的。

我们继续查看SkiaVulkanPipeline的实现,同时也是trace上flush commands的入口点:

SkiaVulkanPipeline.cpp

cpp 复制代码
IRenderPipeline::DrawResult SkiaVulkanPipeline::draw(
    const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
    const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
    const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
    const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
    const HardwareBufferRenderParams& bufferParams) {
    sk_sp<SkSurface> backBuffer;
    SkMatrix preTransform;
    if (mHardwareBuffer) {
        backBuffer = getBufferSkSurface(bufferParams);
        preTransform = bufferParams.getTransform();
    } else {
        backBuffer = mVkSurface->getCurrentSkSurface();
        preTransform = mVkSurface->getCurrentPreTransform();
    }

    if (backBuffer.get() == nullptr) {
        return {false, -1};
    }

    // update the coordinates of the global light position based on surface rotation
    SkPoint lightCenter = preTransform.mapXY(lightGeometry.center.x, lightGeometry.center.y);
    LightGeometry localGeometry = lightGeometry;
    localGeometry.center.x = lightCenter.fX;
    localGeometry.center.y = lightCenter.fY;

    LightingInfo::updateLighting(localGeometry, lightInfo);
    renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer,
                preTransform);

    // Draw visual debugging features
    if (CC_UNLIKELY(Properties::showDirtyRegions ||
                    ProfileType::None != Properties::getProfileType())) {
        SkCanvas* profileCanvas = backBuffer->getCanvas();
        SkAutoCanvasRestore saver(profileCanvas, true);
        profileCanvas->concat(mVkSurface->getCurrentPreTransform());
        SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height());
        profiler->draw(profileRenderer);
    }

    nsecs_t submissionTime = IRenderPipeline::DrawResult::kUnknownTime;
    {
        ATRACE_NAME("flush commands");
        submissionTime = vulkanManager().finishFrame(backBuffer.get());
    }
    layerUpdateQueue->clear();

    // Log memory statistics
    if (CC_UNLIKELY(Properties::debugLevel != kDebugDisabled)) {
        dumpResourceCacheUsage();
    }

    return {true, submissionTime};
}

这里调用了父类SkiaPipelinerenderFrame方法:

SkiaPipeline.cpp

cpp 复制代码
void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip,
                               const std::vector<sp<RenderNode>>& nodes, bool opaque,
                               const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
                               const SkMatrix& preTransform) {
    bool previousSkpEnabled = Properties::skpCaptureEnabled;
    if (mPictureCapturedCallback) {
        Properties::skpCaptureEnabled = true;
    }

    // Initialize the canvas for the current frame, that might be a recording canvas if SKP
    // capture is enabled.
    SkCanvas* canvas = tryCapture(surface.get(), nodes[0].get(), layers);

    // draw all layers up front
    renderLayersImpl(layers, opaque);

    renderFrameImpl(clip, nodes, opaque, contentDrawBounds, canvas, preTransform);

    endCapture(surface.get());

    if (CC_UNLIKELY(Properties::debugOverdraw)) {
        renderOverdraw(clip, nodes, contentDrawBounds, surface, preTransform);
    }

    Properties::skpCaptureEnabled = previousSkpEnabled;
}


void SkiaPipeline::renderFrameImpl(const SkRect& clip,
                                   const std::vector<sp<RenderNode>>& nodes, bool opaque,
                                   const Rect& contentDrawBounds, SkCanvas* canvas,
                                   const SkMatrix& preTransform) {
    SkAutoCanvasRestore saver(canvas, true);
    auto clipRestriction = preTransform.mapRect(clip).roundOut();
    if (CC_UNLIKELY(isCapturingSkp())) {
        canvas->drawAnnotation(SkRect::Make(clipRestriction), "AndroidDeviceClipRestriction",
            nullptr);
    } else {
        // clip drawing to dirty region only when not recording SKP files (which should contain all
        // draw ops on every frame)
        canvas->androidFramework_setDeviceClipRestriction(clipRestriction);
    }
    canvas->concat(preTransform);

    if (!opaque) {
        canvas->clear(SK_ColorTRANSPARENT);
    }

    if (1 == nodes.size()) {
        if (!nodes[0]->nothingToDraw()) {
            RenderNodeDrawable root(nodes[0].get(), canvas);
            root.draw(canvas);
        }
    } else if (0 == nodes.size()) {
        // nothing to draw
    } else {
        // It there are multiple render nodes, they are laid out as follows:
        // #0 - backdrop (content + caption)
        // #1 - content (local bounds are at (0,0), will be translated and clipped to backdrop)
        // #2 - additional overlay nodes
        // Usually the backdrop cannot be seen since it will be entirely covered by the content.
        // While
        // resizing however it might become partially visible. The following render loop will crop
        // the
        // backdrop against the content and draw the remaining part of it. It will then draw the
        // content
        // cropped to the backdrop (since that indicates a shrinking of the window).
        //
        // Additional nodes will be drawn on top with no particular clipping semantics.

        // Usually the contents bounds should be mContentDrawBounds - however - we will
        // move it towards the fixed edge to give it a more stable appearance (for the moment).
        // If there is no content bounds we ignore the layering as stated above and start with 2.

        // Backdrop bounds in render target space
        const Rect backdrop = nodeBounds(*nodes[0]);

        // Bounds that content will fill in render target space (note content node bounds may be
        // bigger)
        Rect content(contentDrawBounds.getWidth(), contentDrawBounds.getHeight());
        content.translate(backdrop.left, backdrop.top);
        if (!content.contains(backdrop) && !nodes[0]->nothingToDraw()) {
            // Content doesn't entirely overlap backdrop, so fill around content (right/bottom)

            // Note: in the future, if content doesn't snap to backdrop's left/top, this may need to
            // also fill left/top. Currently, both 2up and freeform position content at the top/left
            // of
            // the backdrop, so this isn't necessary.
            RenderNodeDrawable backdropNode(nodes[0].get(), canvas);
            if (content.right < backdrop.right) {
                // draw backdrop to right side of content
                SkAutoCanvasRestore acr(canvas, true);
                canvas->clipRect(SkRect::MakeLTRB(content.right, backdrop.top, backdrop.right,
                                                  backdrop.bottom));
                backdropNode.draw(canvas);
            }
            if (content.bottom < backdrop.bottom) {
                // draw backdrop to bottom of content
                // Note: bottom fill uses content left/right, to avoid overdrawing left/right fill
                SkAutoCanvasRestore acr(canvas, true);
                canvas->clipRect(SkRect::MakeLTRB(content.left, content.bottom, content.right,
                                                  backdrop.bottom));
                backdropNode.draw(canvas);
            }
        }

        RenderNodeDrawable contentNode(nodes[1].get(), canvas);
        if (!backdrop.isEmpty()) {
            // content node translation to catch up with backdrop
            float dx = backdrop.left - contentDrawBounds.left;
            float dy = backdrop.top - contentDrawBounds.top;

            SkAutoCanvasRestore acr(canvas, true);
            canvas->translate(dx, dy);
            const SkRect contentLocalClip =
                    SkRect::MakeXYWH(contentDrawBounds.left, contentDrawBounds.top,
                                     backdrop.getWidth(), backdrop.getHeight());
            canvas->clipRect(contentLocalClip);
            contentNode.draw(canvas);
        } else {
            SkAutoCanvasRestore acr(canvas, true);
            contentNode.draw(canvas);
        }

        // remaining overlay nodes, simply defer
        for (size_t index = 2; index < nodes.size(); index++) {
            if (!nodes[index]->nothingToDraw()) {
                SkAutoCanvasRestore acr(canvas, true);
                RenderNodeDrawable overlayNode(nodes[index].get(), canvas);
                overlayNode.draw(canvas);
            }
        }
    }
}

继续调用了RenderNodeDrawabledrawContent方法实现内容的渲染:

RenderNodeDrawable.cpp

cpp 复制代码
void RenderNodeDrawable::drawContent(SkCanvas* canvas) const {
    RenderNode* renderNode = mRenderNode.get();
    float alphaMultiplier = 1.0f;
    const RenderProperties& properties = renderNode->properties();

    // If we are drawing the contents of layer, we don't want to apply any of
    // the RenderNode's properties during this pass. Those will all be applied
    // when the layer is composited.
    if (mComposeLayer) {
        setViewProperties(properties, canvas, &alphaMultiplier);
    }
    SkiaDisplayList* displayList = mRenderNode->getDisplayList().asSkiaDl();
    displayList->mParentMatrix = canvas->getTotalMatrix();

    // TODO should we let the bound of the drawable do this for us?
    const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
    bool quickRejected = properties.getClipToBounds() && canvas->quickReject(bounds);
    if (!quickRejected) {
        auto clipBounds = canvas->getLocalClipBounds();
        SkIRect srcBounds = SkIRect::MakeWH(bounds.width(), bounds.height());
        SkIPoint offset = SkIPoint::Make(0.0f, 0.0f);
        SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl();
        const LayerProperties& layerProperties = properties.layerProperties();
        // composing a hardware layer
        if (renderNode->getLayerSurface() && mComposeLayer) {
            SkASSERT(properties.effectiveLayerType() == LayerType::RenderLayer);
            SkPaint paint;
            layerNeedsPaint(layerProperties, alphaMultiplier, &paint);
            sk_sp<SkImage> snapshotImage;
            auto* imageFilter = layerProperties.getImageFilter();
            auto recordingContext = canvas->recordingContext();
            // On some GL vendor implementations, caching the result of
            // getLayerSurface->makeImageSnapshot() causes a call to
            // Fence::waitForever without a corresponding signal. This would
            // lead to ANRs throughout the system.
            // Instead only cache the SkImage created with the SkImageFilter
            // for supported devices. Otherwise just create a new SkImage with
            // the corresponding SkImageFilter each time.
            // See b/193145089 and b/197263715
            if (!Properties::enableRenderEffectCache) {
                snapshotImage = renderNode->getLayerSurface()->makeImageSnapshot();
                if (imageFilter) {
                    auto subset = SkIRect::MakeWH(srcBounds.width(), srcBounds.height());
                    snapshotImage = snapshotImage->makeWithFilter(recordingContext, imageFilter,
                                                                  subset, clipBounds.roundOut(),
                                                                  &srcBounds, &offset);
                }
            } else {
                const auto snapshotResult = renderNode->updateSnapshotIfRequired(
                        recordingContext, layerProperties.getImageFilter(), clipBounds.roundOut());
                snapshotImage = snapshotResult->snapshot;
                srcBounds = snapshotResult->outSubset;
                offset = snapshotResult->outOffset;
            }

            const auto dstBounds = SkIRect::MakeXYWH(offset.x(),
                                                     offset.y(),
                                                     srcBounds.width(),
                                                     srcBounds.height());
            SkSamplingOptions sampling(SkFilterMode::kLinear);

            // surfaces for layers are created on LAYER_SIZE boundaries (which are >= layer size) so
            // we need to restrict the portion of the surface drawn to the size of the renderNode.
            SkASSERT(renderNode->getLayerSurface()->width() >= bounds.width());
            SkASSERT(renderNode->getLayerSurface()->height() >= bounds.height());

            // If SKP recording is active save an annotation that indicates this drawImageRect
            // could also be rendered with the commands saved at ID associated with this node.
            if (CC_UNLIKELY(Properties::skpCaptureEnabled)) {
                canvas->drawAnnotation(bounds, String8::format(
                    "SurfaceID|%" PRId64, renderNode->uniqueId()).c_str(), nullptr);
            }

            const StretchEffect& stretch = properties.layerProperties().getStretchEffect();
            if (stretch.isEmpty() ||
                Properties::getStretchEffectBehavior() == StretchEffectBehavior::UniformScale) {
                // If we don't have any stretch effects, issue the filtered
                // canvas draw calls to make sure we still punch a hole
                // with the same canvas transformation + clip into the target
                // canvas then draw the layer on top
                if (renderNode->hasHolePunches()) {
                    canvas->save();
                    TransformCanvas transformCanvas(canvas, SkBlendMode::kDstOut);
                    displayList->draw(&transformCanvas);
                    canvas->restore();
                }
                canvas->drawImageRect(snapshotImage, SkRect::Make(srcBounds),
                                      SkRect::Make(dstBounds), sampling, &paint,
                                      SkCanvas::kStrict_SrcRectConstraint);
            } else {
                // If we do have stretch effects and have hole punches,
                // then create a mask and issue the filtered draw calls to
                // get the corresponding hole punches.
                // Then apply the stretch to the mask and draw the mask to
                // the destination
                // Also if the stretchy container has an ImageFilter applied
                // to it (i.e. blur) we need to take into account the offset
                // that will be generated with this result. Ex blurs will "grow"
                // the source image by the blur radius so we need to translate
                // the shader by the same amount to render in the same location
                SkMatrix matrix;
                matrix.setTranslate(
                    offset.x() - srcBounds.left(),
                    offset.y() - srcBounds.top()
                );
                if (renderNode->hasHolePunches()) {
                    GrRecordingContext* context = canvas->recordingContext();
                    StretchMask& stretchMask = renderNode->getStretchMask();
                    stretchMask.draw(context,
                                     stretch,
                                     bounds,
                                     displayList,
                                     canvas);
                }

                sk_sp<SkShader> stretchShader =
                        stretch.getShader(bounds.width(), bounds.height(), snapshotImage, &matrix);
                paint.setShader(stretchShader);
                canvas->drawRect(SkRect::Make(dstBounds), paint);
            }

            if (!renderNode->getSkiaLayer()->hasRenderedSinceRepaint) {
                renderNode->getSkiaLayer()->hasRenderedSinceRepaint = true;
                if (CC_UNLIKELY(Properties::debugLayersUpdates)) {
                    SkPaint layerPaint;
                    layerPaint.setColor(0x7f00ff00);
                    canvas->drawRect(bounds, layerPaint);
                } else if (CC_UNLIKELY(Properties::debugOverdraw)) {
                    // Render transparent rect to increment overdraw for repaint area.
                    // This can be "else if" because flashing green on layer updates
                    // will also increment the overdraw if it happens to be turned on.
                    SkPaint transparentPaint;
                    transparentPaint.setColor(SK_ColorTRANSPARENT);
                    canvas->drawRect(bounds, transparentPaint);
                }
            }
        } else {
            if (alphaMultiplier < 1.0f) {
                // Non-layer draw for a view with getHasOverlappingRendering=false, will apply
                // the alpha to the paint of each nested draw.
                AlphaFilterCanvas alphaCanvas(canvas, alphaMultiplier);
                displayList->draw(&alphaCanvas);
            } else {
                displayList->draw(canvas);
            }
        }
    }
}

继续调用SkiaDisplayListdraw方法:

SkiaDisplayList.h

cpp 复制代码
    DisplayListData mDisplayList;

    void draw(SkCanvas* canvas) { mDisplayList.draw(canvas); }

DisplayListData::draw调用了各个Opdraw方法,DrawPathdraw方法又调用了SkCanvasdrawPath方法

cpp 复制代码
static const draw_fn draw_fns[] = {
#include "DisplayListOps.in"
};

template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
    auto end = fBytes.get() + fUsed;
    for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
        auto op = (const Op*)ptr;
        auto type = op->type;
        auto skip = op->skip;
        if (auto fn = fns[type]) {  // We replace no-op functions with nullptrs
            fn(op, args...);        // to avoid the overhead of a pointless call.
        }
        ptr += skip;
    }
}

struct DrawPath final : Op {
    static const auto kType = Type::DrawPath;
    DrawPath(const SkPath& path, const SkPaint& paint) : path(path), paint(paint) {}
    SkPath path;
    SkPaint paint;
    void draw(SkCanvas* c, const SkMatrix&) const { c->drawPath(path, paint); }
};

void DisplayListData::draw(SkCanvas* canvas) const {
    SkAutoCanvasRestore acr(canvas, false);
    this->map(draw_fns, canvas, canvas->getTotalMatrix());
}

// All ops implement draw().
#define X(T)                                                    \
    [](const void* op, SkCanvas* c, const SkMatrix& original) { \
        ((const T*)op)->draw(c, original);                      \
    },
static const draw_fn draw_fns[] = {
#include "DisplayListOps.in"
};
#undef X

struct DrawPath final : Op {
    static const auto kType = Type::DrawPath;
    DrawPath(const SkPath& path, const SkPaint& paint) : path(path), paint(paint) {}
    SkPath path;
    SkPaint paint;
    void draw(SkCanvas* c, const SkMatrix&) const { c->drawPath(path, paint); }
};

DisplayListOps.in

cpp 复制代码
X(Flush)
X(Save)
X(Restore)
X(SaveLayer)
X(SaveBehind)
X(Concat)
X(SetMatrix)
X(Scale)
X(Translate)
X(ClipPath)
X(ClipRect)
X(ClipRRect)
X(ClipRegion)
X(ResetClip)
X(DrawPaint)
X(DrawBehind)
X(DrawPath)
X(DrawRect)
X(DrawRegion)
X(DrawOval)
X(DrawArc)
X(DrawRRect)
X(DrawDRRect)
X(DrawAnnotation)
X(DrawDrawable)
X(DrawPicture)
X(DrawImage)
X(DrawImageRect)
X(DrawImageLattice)
X(DrawTextBlob)
X(DrawPatch)
X(DrawPoints)
X(DrawVertices)
X(DrawAtlas)
X(DrawShadowRec)
X(DrawVectorDrawable)
X(DrawRippleDrawable)
X(DrawWebView)
X(DrawSkMesh)
X(DrawMesh)

SkCanvas.cpp

cpp 复制代码
void SkCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
    TRACE_EVENT0("skia", TRACE_FUNC);
    this->onDrawPath(path, paint);
}

void SkCanvas::onDrawPath(const SkPath& path, const SkPaint& paint) {
    if (!path.isFinite()) {
        return;
    }

    const SkRect& pathBounds = path.getBounds();
    if (!path.isInverseFillType() && this->internalQuickReject(pathBounds, paint)) {
        return;
    }
    if (path.isInverseFillType() && pathBounds.width() <= 0 && pathBounds.height() <= 0) {
        this->internalDrawPaint(paint);
        return;
    }

    auto layer = this->aboutToDraw(this, paint, path.isInverseFillType() ? nullptr : &pathBounds);
    if (layer) {
        this->topDevice()->drawPath(path, layer->paint());
    }
}

这里需要确定SkiaPipeline::renderFrame中tryCapture返回的SkCanvas实例是什么,才能知道topDevice()拿到的是什么。

实际的调用堆栈为:

Plain 复制代码
SkCanvas::init(sk_sp<SkBaseDevice>)
SkCanvas::SkCanvas(sk_sp<SkBaseDevice>))
SkSurface_Gpu::onNewCanvas()
SkSurface::getCanvas()
SkiaPipeline.tryCapture(SkSurface*)
SkiaPipeline.renderFrame()

因此SkCanvas::drawPaththis->topDevice()->drawPath对应于genesh 中的Device::drawPath

external/skia/src/gpu/ganesh/Device.cpp

cpp 复制代码
void Device::drawPath(const SkPath& origSrcPath, const SkPaint& paint, bool pathIsMutable) {
#if GR_TEST_UTILS
    if (fContext->priv().options().fAllPathsVolatile && !origSrcPath.isVolatile()) {
        this->drawPath(SkPath(origSrcPath).setIsVolatile(true), paint, true);
        return;
    }
#endif
    ASSERT_SINGLE_OWNER
    GR_CREATE_TRACE_MARKER_CONTEXT("skgpu::v1::Device", "drawPath", fContext.get());
    if (!paint.getMaskFilter()) {
        GrPaint grPaint;
        if (!SkPaintToGrPaint(this->recordingContext(),
                              fSurfaceDrawContext->colorInfo(),
                              paint,
                              this->localToDevice(),
                              fSurfaceDrawContext->surfaceProps(),
                              &grPaint)) {
            return;
        }
        fSurfaceDrawContext->drawPath(this->clip(), std::move(grPaint),
                                      fSurfaceDrawContext->chooseAA(paint), this->localToDevice(),
                                      origSrcPath, GrStyle(paint));
        return;
    }

    // TODO: losing possible mutability of 'origSrcPath' here
    GrStyledShape shape(origSrcPath, paint);

    GrBlurUtils::drawShapeWithMaskFilter(fContext.get(), fSurfaceDrawContext.get(), this->clip(),
                                         paint, this->asMatrixProvider(), shape);
}

这里继续调用SurfaceDrawContextdrawPath方法:

SurfaceDrawContext.cpp

cpp 复制代码
void SurfaceDrawContext::drawPath(const GrClip* clip,
                                  GrPaint&& paint,
                                  GrAA aa,
                                  const SkMatrix& viewMatrix,
                                  const SkPath& path,
                                  const GrStyle& style) {
    ASSERT_SINGLE_OWNER
    RETURN_IF_ABANDONED
    SkDEBUGCODE(this->validate();)
    GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceDrawContext", "drawPath", fContext);

    GrStyledShape shape(path, style, DoSimplify::kNo);
    this->drawShape(clip, std::move(paint), aa, viewMatrix, std::move(shape));
}

void SurfaceDrawContext::drawShape(const GrClip* clip,
                                   GrPaint&& paint,
                                   GrAA aa,
                                   const SkMatrix& viewMatrix,
                                   GrStyledShape&& shape) {
    ASSERT_SINGLE_OWNER
    RETURN_IF_ABANDONED
    SkDEBUGCODE(this->validate();)
    GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceDrawContext", "drawShape", fContext);

    if (shape.isEmpty()) {
        if (shape.inverseFilled()) {
            this->drawPaint(clip, std::move(paint), viewMatrix);
        }
        return;
    }

    AutoCheckFlush acf(this->drawingManager());

    // If we get here in drawShape(), we definitely need to use path rendering
    this->drawShapeUsingPathRenderer(clip, std::move(paint), aa, viewMatrix, std::move(shape),
                                     /* attemptDrawSimple */ true);
}


void SurfaceDrawContext::drawShapeUsingPathRenderer(const GrClip* clip,
                                                    GrPaint&& paint,
                                                    GrAA aa,
                                                    const SkMatrix& viewMatrix,
                                                    GrStyledShape&& shape,
                                                    bool attemptDrawSimple) {
    ASSERT_SINGLE_OWNER
    RETURN_IF_ABANDONED
    GR_CREATE_TRACE_MARKER_CONTEXT("SurfaceDrawContext", "internalDrawPath", fContext);

    if (!viewMatrix.isFinite() || !shape.bounds().isFinite()) {
        return;
    }

    SkIRect clipConservativeBounds = get_clip_bounds(this, clip);

    // Always allow paths to trigger DMSAA.
    GrAAType aaType = fCanUseDynamicMSAA ? GrAAType::kMSAA : this->chooseAAType(aa);

    PathRenderer::CanDrawPathArgs canDrawArgs;
    canDrawArgs.fCaps = this->caps();
    canDrawArgs.fProxy = this->asRenderTargetProxy();
    canDrawArgs.fViewMatrix = &viewMatrix;
    canDrawArgs.fShape = &shape;
    canDrawArgs.fPaint = &paint;
    canDrawArgs.fSurfaceProps = &fSurfaceProps;
    canDrawArgs.fClipConservativeBounds = &clipConservativeBounds;
    canDrawArgs.fHasUserStencilSettings = false;
    canDrawArgs.fAAType = aaType;

    constexpr static bool kDisallowSWPathRenderer = false;
    constexpr static bool kAllowSWPathRenderer = true;
    using DrawType = PathRendererChain::DrawType;

    PathRenderer* pr = nullptr;

    if (!shape.style().strokeRec().isFillStyle() && !shape.isEmpty()) {
        // Give the tessellation path renderer a chance to claim this stroke before we simplify it.
        PathRenderer* tess = this->drawingManager()->getTessellationPathRenderer();
        if (tess && tess->canDrawPath(canDrawArgs) == PathRenderer::CanDrawPath::kYes) {
            pr = tess;
        }
    }

    if (!pr) {
        // The shape isn't a stroke that can be drawn directly. Simplify if possible.
        shape.simplify();

        if (shape.isEmpty() && !shape.inverseFilled()) {
            return;
        }

        if (attemptDrawSimple || shape.simplified()) {
            // Usually we enter drawShapeUsingPathRenderer() because the shape+style was too complex
            // for dedicated draw ops. However, if GrStyledShape was able to reduce something we
            // ought to try again instead of going right to path rendering.
            if (this->drawSimpleShape(clip, &paint, aa, viewMatrix, shape)) {
                return;
            }
        }

        // Try a 1st time without applying any of the style to the geometry (and barring sw)
        pr = this->drawingManager()->getPathRenderer(canDrawArgs, kDisallowSWPathRenderer,
                                                     DrawType::kColor);
    }

    SkScalar styleScale =  GrStyle::MatrixToScaleFactor(viewMatrix);
    if (styleScale == 0.0f) {
        return;
    }

    if (!pr && shape.style().pathEffect()) {
        // It didn't work above, so try again with the path effect applied.
        shape = shape.applyStyle(GrStyle::Apply::kPathEffectOnly, styleScale);
        if (shape.isEmpty()) {
            return;
        }
        pr = this->drawingManager()->getPathRenderer(canDrawArgs, kDisallowSWPathRenderer,
                                                     DrawType::kColor);
    }
    if (!pr) {
        if (shape.style().applies()) {
            shape = shape.applyStyle(GrStyle::Apply::kPathEffectAndStrokeRec, styleScale);
            if (shape.isEmpty()) {
                return;
            }
            // This time, allow SW renderer
            pr = this->drawingManager()->getPathRenderer(canDrawArgs, kAllowSWPathRenderer,
                                                         DrawType::kColor);
        } else {
            pr = this->drawingManager()->getSoftwarePathRenderer();
#if GR_PATH_RENDERER_SPEW
            SkDebugf("falling back to: %s\n", pr->name());
#endif
        }
    }

    if (!pr) {
#ifdef SK_DEBUG
        SkDebugf("Unable to find path renderer compatible with path.\n");
#endif
        return;
    }

    PathRenderer::DrawPathArgs args{this->drawingManager()->getContext(),
                                    std::move(paint),
                                    &GrUserStencilSettings::kUnused,
                                    this,
                                    clip,
                                    &clipConservativeBounds,
                                    &viewMatrix,
                                    canDrawArgs.fShape,
                                    aaType,
                                    this->colorInfo().isLinearlyBlended()};
    pr->drawPath(args);
}

这里通过GrDrawingManager获取一个合适的PathRenderer实例进行渲染:

GrDrawingManager.cpp

cpp 复制代码
/*
 * This method finds a path renderer that can draw the specified path on
 * the provided target.
 * Due to its expense, the software path renderer has split out so it can
 * can be individually allowed/disallowed via the "allowSW" boolean.
 */
skgpu::v1::PathRenderer* GrDrawingManager::getPathRenderer(
        const PathRenderer::CanDrawPathArgs& args,
        bool allowSW,
        PathRendererChain::DrawType drawType,
        PathRenderer::StencilSupport* stencilSupport) {

    if (!fPathRendererChain) {
        fPathRendererChain =
                std::make_unique<PathRendererChain>(fContext, fOptionsForPathRendererChain);
    }

    auto pr = fPathRendererChain->getPathRenderer(args, drawType, stencilSupport);
    if (!pr && allowSW) {
        auto swPR = this->getSoftwarePathRenderer();
        if (PathRenderer::CanDrawPath::kNo != swPR->canDrawPath(args)) {
            pr = swPR;
        }
    }

#if GR_PATH_RENDERER_SPEW
    if (pr) {
        SkDebugf("getPathRenderer: %s\n", pr->name());
    }
#endif

    return pr;
}

调用PathRendererChaingetPathRenderer方法获取PathRenderer:

PathRendererChain.cpp

cpp 复制代码
PathRendererChain::PathRendererChain(GrRecordingContext* context, const Options& options) {
    const GrCaps& caps = *context->priv().caps();
    if (options.fGpuPathRenderers & GpuPathRenderers::kDashLine) {
        fChain.push_back(sk_make_sp<ganesh::DashLinePathRenderer>());
    }
    if (options.fGpuPathRenderers & GpuPathRenderers::kAAConvex) {
        fChain.push_back(sk_make_sp<AAConvexPathRenderer>());
    }
    if (options.fGpuPathRenderers & GpuPathRenderers::kAAHairline) {
        fChain.push_back(sk_make_sp<AAHairLinePathRenderer>());
    }
    if (options.fGpuPathRenderers & GpuPathRenderers::kAALinearizing) {
        fChain.push_back(sk_make_sp<AALinearizingConvexPathRenderer>());
    }
    if (options.fGpuPathRenderers & GpuPathRenderers::kAtlas) {
        if (auto atlasPathRenderer = AtlasPathRenderer::Make(context)) {
            fAtlasPathRenderer = atlasPathRenderer.get();
            context->priv().addOnFlushCallbackObject(atlasPathRenderer.get());
            fChain.push_back(std::move(atlasPathRenderer));
        }
    }
#if !defined(SK_ENABLE_OPTIMIZE_SIZE)
    if (options.fGpuPathRenderers & GpuPathRenderers::kSmall) {
        fChain.push_back(sk_make_sp<SmallPathRenderer>());
    }
    if (options.fGpuPathRenderers & GpuPathRenderers::kTriangulating) {
        fChain.push_back(sk_make_sp<TriangulatingPathRenderer>());
    }
#endif
    if (options.fGpuPathRenderers & GpuPathRenderers::kTessellation) {
        if (TessellationPathRenderer::IsSupported(caps)) {
            auto tess = sk_make_sp<TessellationPathRenderer>();
            fTessellationPathRenderer = tess.get();
            fChain.push_back(std::move(tess));
        }
    }

    // We always include the default path renderer (as well as SW), so we can draw any path
    fChain.push_back(sk_make_sp<DefaultPathRenderer>());
}


PathRenderer* PathRendererChain::getPathRenderer(const PathRenderer::CanDrawPathArgs& args,
                                                 DrawType drawType,
                                                 PathRenderer::StencilSupport* stencilSupport) {
    static_assert(PathRenderer::kNoSupport_StencilSupport <
                  PathRenderer::kStencilOnly_StencilSupport);
    static_assert(PathRenderer::kStencilOnly_StencilSupport <
                  PathRenderer::kNoRestriction_StencilSupport);
    PathRenderer::StencilSupport minStencilSupport;
    if (DrawType::kStencil == drawType) {
        minStencilSupport = PathRenderer::kStencilOnly_StencilSupport;
    } else if (DrawType::kStencilAndColor == drawType) {
        minStencilSupport = PathRenderer::kNoRestriction_StencilSupport;
    } else {
        minStencilSupport = PathRenderer::kNoSupport_StencilSupport;
    }
    if (minStencilSupport != PathRenderer::kNoSupport_StencilSupport) {
        // We don't support (and shouldn't need) stenciling of non-fill paths.
        if (!args.fShape->style().isSimpleFill()) {
            return nullptr;
        }
    }

    PathRenderer* bestPathRenderer = nullptr;
    for (const sk_sp<PathRenderer>& pr : fChain) {
        PathRenderer::StencilSupport support = PathRenderer::kNoSupport_StencilSupport;
        if (PathRenderer::kNoSupport_StencilSupport != minStencilSupport) {
            support = pr->getStencilSupport(*args.fShape);
            if (support < minStencilSupport) {
                continue;
            }
        }
        PathRenderer::CanDrawPath canDrawPath = pr->canDrawPath(args);
        if (PathRenderer::CanDrawPath::kNo == canDrawPath) {
            continue;
        }
        if (PathRenderer::CanDrawPath::kAsBackup == canDrawPath && bestPathRenderer) {
            continue;
        }
        if (stencilSupport) {
            *stencilSupport = support;
        }
        bestPathRenderer = pr.get();
        if (PathRenderer::CanDrawPath::kYes == canDrawPath) {
            break;
        }
    }
    return bestPathRenderer;
}

从trace的具体调用栈信息中我们知道最终使用的AtlasPathRenderer,其具体实现为:

AtlasPathRenderer.cpp

cpp 复制代码
bool AtlasPathRenderer::onDrawPath(const DrawPathArgs& args) {
    SkPath path;
    args.fShape->asPath(&path);

    const SkRect pathDevBounds = args.fViewMatrix->mapRect(args.fShape->bounds());
    SkASSERT(this->pathFitsInAtlas(pathDevBounds, args.fAAType));

    if (!is_visible(pathDevBounds, args.fClip->getConservativeBounds())) {
        // The path is empty or outside the clip. No mask is needed.
        if (path.isInverseFillType()) {
            args.fSurfaceDrawContext->drawPaint(args.fClip, std::move(args.fPaint),
                                                *args.fViewMatrix);
        }
        return true;
    }

    SkIRect devIBounds;
    SkIPoint16 locationInAtlas;
    bool transposedInAtlas;
    SkAssertResult(this->addPathToAtlas(args.fContext, *args.fViewMatrix, path, pathDevBounds,
                                        &devIBounds, &locationInAtlas, &transposedInAtlas,
                                        nullptr/*DrawRefsAtlasCallback -- see onCanDrawPath()*/));

    const SkIRect& fillBounds = args.fShape->inverseFilled()
            ? (args.fClip
                    ? args.fClip->getConservativeBounds()
                    : args.fSurfaceDrawContext->asSurfaceProxy()->backingStoreBoundsIRect())
            : devIBounds;
    const GrCaps& caps = *args.fSurfaceDrawContext->caps();
    auto op = GrOp::Make<DrawAtlasPathOp>(args.fContext,
                                          args.fSurfaceDrawContext->arenaAlloc(),
                                          fillBounds, *args.fViewMatrix,
                                          std::move(args.fPaint), locationInAtlas,
                                          devIBounds, transposedInAtlas,
                                          fAtlasRenderTasks.back()->readView(caps),
                                          args.fShape->inverseFilled());
    args.fSurfaceDrawContext->addDrawOp(args.fClip, std::move(op));
    return true;
}

至此,我们从RenderThreadDrawFrameTask 追踪到了具体的Path渲染器实现类AtlasPathRenderer 。同时我们从PathRendererChain 的实现看到了底层是有众多Path渲染器的,PathRendererChain是通过待渲染Path的具体特征选择合适的Path渲染器进行渲染的。

关于2D Path的客户端渲染

其实,2D Path的渲染在客户端上一直是一个难题,因为Path是由一系列指令(lineToarcToquanTocubicTo等)组成的复杂矢量图形,同时还包括不同的渲染模式(stroke、fill),天然不适合GPU进行处理。因此其渲染在早期是在CPU侧直接完成的,近期才支持硬件加速渲染的。

目前的2D Path硬件加速渲染主要有两种实现方式: 硬件驱动实现、软件变换+硬件驱动实现。

硬件驱动时间

硬件驱动直接提供渲染2D Path的接口,以Nvidia的 nv_path_rendering OpenGL扩展为代表。

软件变换+硬件驱动实现

实现思路是由软件侧将Path解析、变换成硬件驱动支持的渲染格式,然后将变换后的渲染数据+指令通过硬件驱动发送到具体硬件完成渲染。

这种方案的主要缺点是需要软件侧在CPU上执行Path的解析和变换,然后再上传到GPU侧,解析和变换需要消耗大量的CPU资源(在特定的case上,软件解析和变换的CPU消耗甚至高于直接软件渲染),而数据上传又涉及到了硬件层的数据传输开销。这也是为什么Android中有不同的PathRenderer实现的底层原因。

在Android上,这里的硬件驱动主要为Vulkan和OpenGL,硬件是GPU,支持的渲染格式一般为三角形。

目前主流的渲染引擎都使用这种方案,如skiapathfindernanovgGLyphyMonkVGcairo等。

相关推荐
运维Z叔2 小时前
云安全 | AWS S3存储桶安全设计缺陷分析
android·网络·网络协议·tcp/ip·安全·云计算·aws
Reese_Cool3 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
平凡シンプル3 小时前
安卓 uniapp跨端开发
android·uni-app
elina80134 小时前
安卓实现导入Excel文件
android·excel
严文文-Chris4 小时前
【设计模式-享元】
android·java·设计模式
趋势大仙4 小时前
SQLiteDatabase insert or replace数据不生效
android·数据库
DS小龙哥4 小时前
QT For Android开发-打开PPT文件
android·qt·powerpoint
试行5 小时前
Android实现自定义下拉列表绑定数据
android·java
Dingdangr10 小时前
Android中的Intent的作用
android
技术无疆10 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入