在上一篇文章让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的入口:
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的入口,同时也是主要耗时点:
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的入口点:
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};
}
这里调用了父类SkiaPipeline 的renderFrame
方法:
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);
}
}
}
}
继续调用了RenderNodeDrawable 的drawContent
方法实现内容的渲染:
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);
}
}
}
}
继续调用SkiaDisplayList 的draw
方法:
cpp
DisplayListData mDisplayList;
void draw(SkCanvas* canvas) { mDisplayList.draw(canvas); }
DisplayListData::draw
调用了各个Op
的draw
方法,DrawPath
的draw
方法又调用了SkCanvas
的drawPath
方法
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); }
};
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)
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::drawPath
中this->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);
}
这里继续调用SurfaceDrawContext 的drawPath
方法:
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实例进行渲染:
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;
}
调用PathRendererChain 的getPathRenderer
方法获取PathRenderer:
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,其具体实现为:
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;
}
至此,我们从RenderThread 的DrawFrameTask 追踪到了具体的Path渲染器实现类AtlasPathRenderer 。同时我们从PathRendererChain 的实现看到了底层是有众多Path渲染器的,PathRendererChain是通过待渲染Path的具体特征选择合适的Path渲染器进行渲染的。
关于2D Path的客户端渲染
其实,2D Path的渲染在客户端上一直是一个难题,因为Path是由一系列指令(lineTo
、arcTo
、quanTo
、cubicTo
等)组成的复杂矢量图形,同时还包括不同的渲染模式(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等。