Android上Path的硬件加速渲染流程分析
作者:访客发布时间:2024-01-01分类:程序开发学习浏览:95
在上一篇文章让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执行细节:
单次DrawFrames耗时2ms以内
绘制背景色 + 50 stroked path
绘制逻辑:
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
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};
}
这里调用了父类SkiaPipeline的renderFrame
方法:
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);
}
}
}
}
继续调用了RenderNodeDrawable的drawContent
方法实现内容的渲染:
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);
}
}
}
}
继续调用SkiaDisplayList的draw
方法:
SkiaDisplayList.h
DisplayListData mDisplayList;
void draw(SkCanvas* canvas) { mDisplayList.draw(canvas); }
DisplayListData::draw
调用了各个Op
的draw
方法,DrawPath
的draw
方法又调用了SkCanvas
的drawPath
方法
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::drawPath
中this->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);
}
这里继续调用SurfaceDrawContext的drawPath
方法:
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;
}
调用PathRendererChain的getPathRenderer
方法获取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;
}
至此,我们从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等。
相关推荐
- 轻松上手:
(三)笔记可再编辑 - 如何在iPhone,iPad和Android上使用WordPress应用程序
- 一款简单高效的Android异步框架
- [Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- Android---View中的setMinWidth与setMinimumWidth的踩坑记录
- Android广播如何解决Sending non-protected broadcast问题
- Dart 启动流程解析:探秘梦之起源
- 有关Android Binder面试,你未知的9个秘密
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- Android低功耗蓝牙开发总结
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版