联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

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

作者:访客发布时间:2024-01-01分类:程序开发学习浏览:69


导读:在上一篇文章让AndroidTextureViewCanvas支持硬件加速中,介绍了如何在TextureView中获取支持硬件加速的绘制Canvas,然而后续发现虽然Canva...

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

场景测试

基于Pixel 7(Android 13)

只绘制背景色

绘制逻辑:

canvas.drawColor(Color.WHITE)

RenderThread执行细节:

image.png

单次DrawFrames耗时2ms以内

绘制背景色 + 50 stroked path

绘制逻辑:

canvas.drawColor(Color.WHITE)
pathList.forEach { path ->  
    canvas.drawPath(path.path, paint)  
}

绘制内容:

50path.png

RenderThread执行细节: image.png

draw-bg_and_50path-1.png

draw-bg_and_50path-2.png

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

流程分析

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

DrawFrameTask.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

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

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

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

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

    DisplayListData mDisplayList;

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

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

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

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

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()拿到的是什么。

实际的调用堆栈为:

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

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

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

/*
 * 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

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

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,支持的渲染格式一般为三角形。

目前主流的渲染引擎都使用这种方案,如skia、pathfinder、nanovg、GLyphy、MonkVG、cairo等。


标签:流程硬件加速安卓系统Path


程序开发学习排行
最近发表
网站分类
标签列表