揭開Android Vulkan渲染封印:幀率暴增的底層指令

ps:本文內容較干,建議收藏后反復邊跟進源碼邊思考設計思想。

渲染管線的基礎架構

為什么叫渲染管線?這里是因為整個渲染的過程涉及多道工序,像管道里的流水線一樣,一道一道的處理數據的過程,所以使用渲染管線還是比較形象的。接下來我們來看下渲染的整個架構。

Android的渲染過程需要按照是否開啟硬件加速分別看待,默認是開啟硬件加速的,那么在應用層使用Canvas的一些繪圖API會預先轉換成OpenGL指令或者Vulkan指令,然后由OpenGL/Vulkan直接操作GPU執行像素化的過程。而如果不開啟硬件加速,Canvas的繪圖指令就會調用skia庫直接利用CPU繪制Bitmap圖。

1、開啟硬件加速


DisplayList到底是什么?

class DisplayListData {
? // 基礎繪制指令
? Vector<DisplayListOp*> displayListOps;
? // 子視圖的應用
? Vector<DrawRenderNodeOp*> children;
? // 命令分組<用于 Z 軸排序>
? Vector<Chunk> chunks;
}

這里說的DisplayList中的指令、GPU可執行的指令序列和GPU原生指令有什么不同呢?


Android應用程序窗口的根視圖是虛擬的,抽象為一個Root Render Node。此外,一個視圖如果設置有Background,那么這個Background也會抽象為一個Background Render Node。Root Render Node、Background Render Node和其它真實的子視圖,除了TextureView和軟件渲染的子視圖之外,都具有Display List,并且是通過一個稱為Display List Renderer的對象進行構建的。

TextureView不具有Display List,它們是通過一個稱為Layer Renderer的對象以Open GL紋理的形式來繪制的,不過這個紋理也不是直接就進行渲染的,而是先記錄在父視圖的Display List中以后再進行渲染的。同樣,軟件渲染的子視圖也不具有Display List,它們先繪制在一個Bitmap上,然后這個Bitmap再記錄在父視圖的Display List中以后再進行渲染的。

DisplayList中的指令是一種抽象化的GPU繪制指令(GPU是無法直接使用的,每個View對應一個),包含這些類型的指令(了解即可):

基礎繪制操作

位圖繪制:DrawBitmapOp(無法硬件渲染只能軟件渲染的視圖的Bitmap)

紋理繪制:DrawLayerOp(如TextureView的OpenGL 紋理)

圖形繪制:DrawPathOp、DrawRectOp等(由Canvas.drawXXX()生成)

視圖層級操作

子視圖引用:DrawRenderNodeOp,封裝子視圖的RenderNode,遞歸執行其Display List

背景繪制:Background Render Node的繪制指令(獨立DisplayList)

狀態控制指令

ReorderBarrier:標記后續子視圖需要按照Z軸排序(用于重疊視圖)

InorderBarrier:標記后續子視圖按默認順序排序(無重疊)

Save/Restore:保存/恢復畫布狀態等等

概括起來說,DisplayList存儲的指令=OpenGL/Vulkan命令的預處理抽象,這些指令在渲染線程中被轉換為GPU可執行的OpenGL/Vulkan指令。

CPU可執行的指令序列是指在Render Thread中將Displaylist中的抽象指令轉換成OpenGL/Vulkan的指令。

GPU原生指令是根據不同硬件生成的最基礎的硬件操作指令,比如操作寄存器等。

2、關閉硬件加速

如果將整個執行流程按照從應用到底層GPU操作分層,可以這樣分層:

渲染階段過程(硬件加速)

1、UI線程DisplayList生成

當需要進行畫面繪制的時候(VSync信號來臨)ViewRootImpl.TraversalRunnable.run()收到回調,調用ViewRootImpl.doTraversal()方法,這個方法中調用ViewRootImpl中的performTraversal()方法到performDraw()再到draw()方法,判斷是否開啟硬件加速,如果不開啟就調用drawSoftware();

如果開啟就調用ThreadRenderer.draw()方法,再繼續調用ThreadRenderer的updateRootDisplayList(),然后調到View的updateDisplayListDirty(),然后調到RenderNode.beginRecording()方法,這里面調到RecordingCanvas.obtain()方法,obtain()方法里面通過new RecordingCanvas(),然后通過JNI調用nCreateDisplayListCanvas()具體是調用Native哪個類?方法創建Native層的RecordingCanvas。

回到View的updateDisplayListDirty()方法里,執行完RenderNode.beginRecording()方法后得到RecordingCanvas的實例canvas,然后繼續執行View里面的draw()方法,根據實際情況drawBackground(),再調onDraw(),以及draw子視圖,里面都是調用RecordingCanvas的api,最終這些API都是JNI調用,在Native層調用的時候會將各種操作記錄為各種抽象命令,并不直接進行畫圖。

2、渲染線程的并行化處理

上一步已經構建好DisplayList數據,ThreadedRenderer調用父類(HardwareRenderer)函數syncAndrDrawFrame(),函數里使用JNI調用nSyncAndDrawFrame(...),也就調用到android_graphics_HardwareRenderer.cpp中的android_view_ThreadedRenderer_syncAndDrawFrame()函數。

這個函數里調用RenderProxy.cpp的syncAndDrawFrame()函數,syncAndDrawFrame()函數里調用DrawFrameTask.cpp的drawFrame()函數,此函數調用函數當前類的postAndWait()函數,然后函數里調用RenderThread的queue()函數,將當前Task入到渲染線程的隊列;當任務執行時,回調到DrawFrameTask.cpp中的run()函數,這個函數就是渲染的核心函數:

void DrawFrameTask::run() {
? ...
? if (CC_LIKELY(canDrawThisFrame)) {
? ? ? ? // 渲染上下文CanvasContext.draw()
? ? ? ? context->draw();
? ? } else {
? ? ? ? // wait on fences so tasks don't overlap next frame
? ? ? ? context->waitOnFences();
? ? }
? ...

}

真正的繪制是通過調用ContextCanvas的draw()函數,里面再通過mRenderPipeline->draw()函數將DisplayList命令轉換成GPU命令,mRenderPipeLine這個就是實際實現渲染的管線,他有可能是通過OpenGL來實現或者通過Vulkan來實現,代碼中有三種渲染管線類型(不同Android版本有一些區別):

SkiaOpenGLPipeline:基于OpenGL的Skia渲染管線

使用OpenGL API進行GPU渲染

兼容性好,支持大多數GPU

性能穩定

SkiaVulkanPipeline:基于Vulkan的Skia渲染管線

使用Vulkan API進行GPU渲染

更低的CPU開銷

更好的多線程支持

更現代的GPU API

SkiaCpuPipeline:基于CPU的Skia渲染管線

用于非Android平臺

純CPU渲染,不依賴GPU

用于調試或者特殊環境

那么Android源碼中是怎么選擇使用哪種渲染管線呢,我們看CanvasContext的create()函數:

CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?RenderNode* rootRenderNode, IContextFactory* contextFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?pid_t uiThreadId, pid_t renderThreadId) {
? ? // 根據系統屬性配置獲取使用的渲染管線類型
? ? auto renderType = Properties::getRenderPipelineType();

? ? switch (renderType) {
? ? ? ? case RenderPipelineType::SkiaGL:
? ? ? ? ? ? return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?uiThreadId, renderThreadId);
? ? ? ? case RenderPipelineType::SkiaVulkan:
? ? ? ? ? ? return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?uiThreadId, renderThreadId);
#ifndef __ANDROID__
? ? ? ? case RenderPipelineType::SkiaCpu:
? ? ? ? ? ? return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?std::make_unique<skiapipeline::SkiaCpuPipeline>(thread),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?uiThreadId, renderThreadId);
#endif
? ? ? ? default:
? ? ? ? ? ? LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType);
? ? ? ? ? ? break;
? ? }
? ? return nullptr;
}

目前官方已經全方面切換到Vulkan庫,那我們就以Vulkan庫繼續分析。那么上文中的mRenderPipeline->draw()函數實際就是SkiaVulkanPipeline->draw()函數調用,draw()函數中調用到父類SkiaGpuPipeline的父類SkiaPipeline的renderFrame()函數mark1:下方尋找SkCanvas實例時機時返回到這里,然后繼續調到SkiaPipeline->renderFrameImpl()函數。這個函數里調用RenderNodeDrawable的基類SkDrawable->draw()函數,draw()函數又調到子類RenderNodeDrawable->onDraw()函數;

渲染線程階段時序圖1

接著繼續調到RenderNodeDrawable->forceDraw()函數再到drawContent()函數,函數調用SkiaDisplaylist->draw()函數,draw()函數委托給DisplayListData->draw()函數,DisplayListData的定義在RecordingCanvas.h文件中,實現在RecordingCanvas.cpp文件中,也就是DisplayListData->draw()在RecordingCanvas.cpp的DisplayListData::draw()函數。

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

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;
? ? ?}
?}

作為一個Java開發者,這代碼看過去可能會一臉懵。別急讓我們來稍微拆解下代碼,首先看:【this->map(draw_fns, canvas, canvas->getTotalMatrix());】這一行,調用map函數時傳入的draw_fns是啥?

#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?

這是C++中的一個X宏(X Macro)技術,為了避免造成更多的困惑不再細講,便于Java開發者理解可粗略的認為draw_fns就是一個數組,里面存儲了繪制相關的操作類型函數指針,具體存儲了哪些操作,定義在DisplayListOps.in文件中:

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(ClipShader)
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)

回到map()函數中,結合上面的分析可以理解為fBytes.get()是DisplayList繪圖指令的內存地址塊的首地址,然后不斷遍歷這塊內存取出指令,根據指令的type,結合fns[]函數指針數組,匹配到指令對應的函數指針然后調用對應的函數,我們這里以DrawPath()為例,回到RecordingCanvas.cpp文件中,根據宏定義會調用【((const T*)op)->draw(c, original)】,對應到:

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); }
};

然后調到SkCanvas->drawPath(),函數里繼續調用onDrawPath()函數:

?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(paint, path.isInverseFillType() ? nullptr : &pathBounds);
? ? ?if (layer) {
? ? ? ? ?this->topDevice()->drawPath(path, layer->paint(), false);
? ? ?}
?}

關鍵的一行:this->topDevice()->drawPath(path, layer->paint(), false);?mark0那么topDevice()獲取到的設備是什么呢?代碼跟到這里,我們心中是否有個疑問,從開始渲染的時候選擇Vulkan渲染管線進行渲染,怎么跳來跳去都是在Skia庫中呢?Vulkan不是要直接轉換GPU命令的嗎?在哪里去轉的?

所以我們回到選擇Vulkan渲染管線的代碼的前面,先回到標注為mark1的位置SkiaPipeline的renderFrame()函數:

void SkiaPipeline::renderFrame() {
? ? 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;
}

我們找到canvas是怎么得到的tryCapture(surface.get(), nodes[0].get(), layers);mark2這個函數也就是通過surface中獲取的,那么surface是怎么實例化的?

再往上回到SkiaVulkanPipeline->draw()函數中,

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, std::mutex& profilerLock) {
? ? sk_sp<SkSurface> backBuffer;
? ? SkMatrix preTransform;
? ? if (mHardwareBuffer) {
? ? ? ? backBuffer = getBufferSkSurface(bufferParams);
? ? ? ? preTransform = bufferParams.getTransform();
? ? } else {
? ? ? ? backBuffer = mVkSurface->getCurrentSkSurface();
? ? ? ? preTransform = mVkSurface->getCurrentPreTransform();
? ? }?

? ? ...

? ? renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, backBuffer,
? ? ? ? ? ? ? ? preTransform);

? ? ...

? ? return {true, drawResult.submissionTime, std::move(drawResult.presentFence)};
}

省略了其他的干擾代碼,backBuffer = getBufferSkSurface(bufferParams);?這一行是獲取SkSurface的,我們一直將代碼跟進去,這里省略了,有興趣的同學自行去跟下源碼,跳轉實在是太多了。最終跳到SkSurface_Ganesh.cpp類中:

sk_sp<SkSurface> WrapBackendTexture() {
? ? ...
? ? auto device = rContext->priv().createDevice(grColorType,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? std::move(proxy),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? std::move(colorSpace),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? origin,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? SkSurfacePropsCopyOrDefault(props),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? skgpu::ganesh::Device::InitContents::kUninit);
? ? ...

? ? return sk_make_sp<SkSurface_Ganesh>(std::move(device));
}

可以看到device實例的創建代碼了,繼續跟進調進了GrRecordingContextPriv.cpp這個類:

sk_sp<skgpu::ganesh::Device> GrRecordingContextPriv::createDevice() {
? ? return skgpu::ganesh::Device::Make(this->context(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?budgeted,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ii,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fit,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?sampleCount,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?mipmapped,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?isProtected,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?origin,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?props,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?init);
}

可以看到是創建skgpu::ganesh::Device實例,讓我們再回到SkCanvas.cpp中的onDrawPath()函數中的this->topDevice()->drawPath(path, layer->paint(), false);這里的device和topDevice是不是同一個東西呢?

topDevice()函數中是通過fMCRec->fDevice返回,fMCRec的實例是在SkCanvas->init()函數中實例化的:

void SkCanvas::init(sk_sp<SkDevice> device) {
? ? ...

? ? fMCRec = new (fMCStack.push_back()) MCRec(device.get());

? ? ...
}

而init()函數是在SkCanvas的構造函數中調用的,這里又回到上面mark2的位置,SkCanvas的獲得過程SkiaPipeLine->tryCapture()函數中,通過surface->getCanvas()獲得canvas,一直調到SkSurface_Base.h中定義的onNewCanvas()函數,這個是基類,實現類也就回到了上面的surface的創建過程的分析過程知道為SkSurface_Ganesh.cpp文件,

我們看里面的onNewCanvas()函數的實現,里面的device就是在SkSurface_Ganesh的構造函數中傳入的,也就是我們WrapBackendTexture()函數中創建的device,到這里終于閉環了,所以topDevice()就是skgpu::ganesh::Device實例,

那么繼續skgpu::ganesh::Device->drawPath(),然后調到SurfaceDrawContext->drawPath();因為調用過程比較多,這里省略部分調用鏈,一直到獲得一個適合的Renderer進行渲染,這里我們以畫虛線為例,使用DashLinePathRenderer.cpp->onDrawPath()函數:

bool DashLinePathRenderer::onDrawPath(const DrawPathArgs& args) {

? ? ...
? ? GrOp::Owner op = DashOp::MakeDashLineOp(args.fContext, std::move(args.fPaint),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? *args.fViewMatrix, pts, aaMode, args.fShape->style(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? args.fUserStencilSettings);
? ? if (!op) {
? ? ? ? return false;
? ? }
? ? args.fSurfaceDrawContext->addDrawOp(args.fClip, std::move(op));
? ? return true;
}

這里將繪制指令封裝在DashOp中。至此本階段過程分析完成。

渲染線程階段時序圖2

3、提交的GPU操作命令

接上面生成的命令封裝實例后,調用:

args.fSurfaceDrawContext->addDrawOp(args.fClip, std::move(op));

看方法名猜測是將封裝的操作加到什么地方去,我們繼續跟蹤代碼,即SurfaceDrawContex->addDrawOp函數中,

void SurfaceDrawContext::addDrawOp(const GrClip* clip,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?GrOp::Owner op,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?const std::function<WillAddOpFn>& willAddFn) {
? ? ...
? ? opsTask->addDrawOp(this->drawingManager(), std::move(op), drawNeedsMSAA, analysis,std::move(appliedClip), dstProxyView,GrTextureResolveManager(this->drawingManager()), *this->caps());
? ? ...

}

是通過調用OpsTask->addDrawOp()函數進行添加的,addDrawOp函數中調用recordOp()函數將操作指令記錄到操作鏈OpChains中(包含一些合并優化操作,減少GPU命令的數量),等待flush指令執行任務OpsTask的onExcute函數,flush發生的時機可能有以下幾個:

自動觸發:AutoCheckFlush析構函數,資源壓力檢測,幀緩沖區交換前

手動觸發:SkSurface::flush(),GrContext::flush(),顯示API調用

當GrDrawingManager->flush()函數被調用,函數里調用GrRenderTask->execute()函數,實際調用的是其實現類OpsTask->onExecute()函數:

bool OpsTask::onExecute(GrOpFlushState* flushState) {

? ? const GrCaps& caps = *flushState->gpu()->caps();
? ? GrRenderTarget* renderTarget = proxy->peekRenderTarget();
? ? SkASSERT(renderTarget);

? ? // 創建渲染通道
? ? GrOpsRenderPass* renderPass = create_render_pass(flushState->gpu(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?proxy->peekRenderTarget(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fUsesMSAASurface,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?stencil,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fTargetOrigin,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fClippedContentBounds,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fColorLoadOp,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fLoadClearColor,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?stencilLoadOp,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?stencilStoreOp,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fSampledProxies,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?fRenderPassXferBarriers);

? ? flushState->setOpsRenderPass(renderPass);
? ? renderPass->begin();

? ? GrSurfaceProxyView dstView(sk_ref_sp(this->target(0)), fTargetOrigin, fTargetSwizzle);

? ? // Draw all the generated geometry.
? ? for (const auto& chain : fOpChains) {
? ? ? ? if (!chain.shouldExecute()) {
? ? ? ? ? ? continue;
? ? ? ? }

? ? ? ? GrOpFlushState::OpArgs opArgs(chain.head(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dstView,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? fUsesMSAASurface,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? chain.appliedClip(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? chain.dstProxyView(),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? fRenderPassXferBarriers,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? fColorLoadOp);

? ? ? ? flushState->setOpArgs(&opArgs);
? ? ? ? // 遍歷指令將其轉換成GPU指令
? ? ? ? chain.head()->execute(flushState, chain.bounds());
? ? ? ? flushState->setOpArgs(nullptr);
? ? }

? ? renderPass->end();

? ? // 提交給GPU執行指令
? ? flushState->gpu()->submit(renderPass);
? ? flushState->setOpsRenderPass(nullptr);

? ? return true;
}

chain.head()->execute(flushState, chain.bounds());這一行將按畫虛線為例會調到DashOpImpl->onExecute()函數:

? ? void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override {
? ? ? ? if (!fProgramInfo || !fMesh) {
? ? ? ? ? ? return;
? ? ? ? }

? ? ? ? flushState->bindPipelineAndScissorClip(*fProgramInfo, chainBounds);
? ? ? ? flushState->bindTextures(fProgramInfo->geomProc(), nullptr, fProgramInfo->pipeline());
? ? ? ? flushState->drawMesh(*fMesh);
? ? }

onExecute函數中調用flushState->drawMesh()函數,然后一直調用到GrVkOpsRenderPass.cpp->onDraw()函數,繼續調用到GrVkCommandBuffer.cpp->draw()函數生成Vulkan 的GPU命令,然后回到OpsTask->onExecute()這個函數繼續往下看,通過flushState->gpu()->submit(renderPass);提交到GPU中執行渲染命令。剩下的就交給GPU去執行命令繪制像素了。

總結與展望

Android通過分層抽象,在兼容性與性能間取得完美平衡。每層只需關注相鄰接口,使Vulkan等新技術可無縫接入現有架構。Vulkan通過瓦解GPU驅動瓶頸,將CPU渲染開銷從15ms壓縮至3ms,釋放出12ms/幀的GPU算力空間:

  1. 1.

    指令編譯革命

  • ?

    DisplayList → SPIR-V中間指令(預編譯避免運行時解析)

  • ?

    對比OpenGL:減少80%驅動層校驗指令

  • 2.

    并行化引擎

    • ?

      OpsTask.onExecute()實現OpChain多核分發

    • ?

      渲染通道(RenderPass)無鎖提交使DrawCall并發量提升8倍

  • 3.

    零拷貝控制

    • ?

      GrVkCommandBuffer直接操作設備內存

    • ?

      消除OpenGL的顯存二次拷貝(省去3ms/幀)

    現代圖形架構的核心矛盾是繪制復雜度與幀時間確定性的對抗。Android的解法是:將非確定操作提前(指令編譯),將確定操作并發(管線并行),最終馴服GPU這頭性能猛獸。

    此刻我們正站在渲染技術的奇點:當Vulkan封印揭開,幀率已不是終點,而是重構視覺體驗的起點。未來會有哪些期待?

    • DisplayList預測生成(LSTM模型預判下幀指令)?

    • 將Path計算卸載到DSP處理(節省GPU 30%負載)?

    • 實時光追管線(Vulkan Ray-Tracing擴展)?

    點贊+關注,下一期更精彩!

    本文分析源碼基于最新的AOSP:https://cs.android.com/android/platform/superproject?hl=zh-cn

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/919185.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/919185.shtml
英文地址,請注明出處:http://en.pswp.cn/news/919185.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

HTTP 請求轉發與重定向詳解及其應用(含 Java 示例)

在 Web 開發中&#xff0c;我們經常需要在不同頁面之間跳轉&#xff0c;比如登錄成功后跳到首頁、提交表單后跳到結果頁面。這時&#xff0c;常見的兩種跳轉方式就是 請求轉發&#xff08;Request Forward&#xff09; 和 重定向&#xff08;Redirect&#xff09;。雖然它們都能…

如何將 MCP Server (FastMCP) 配置為公網訪問(監聽 0.0.0.0)

如何將 MCP Server &#xff08;FastMCP&#xff09; 配置為公網訪問&#xff08;監聽 0.0.0.0&#xff09;引言常見錯誤嘗試根本原因&#xff1a;從源碼解析正確的解決方案總結引言 在使用 Model Context Protocol(MCP) 框架開發自定義工具服務器時&#xff0c;我們經常使用 …

The Network Link Layer: 無線傳感器中Delay Tolerant Networks – DTNs 延遲容忍網絡

Delay Tolerant Networks – DTNs 延遲容忍網絡架構歸屬Delay Tolerant Networks – DTNs 延遲容忍網絡應用實例例子 1&#xff1a;瑞典北部的薩米人 (Saami reindeer herders)例子 2&#xff1a;太平洋中的動物傳感網絡DTNs路由方式——存儲&轉發DTNs移動模型Random walk …

計算機視覺(opencv)實戰二——圖像邊界擴展cv2.copyMakeBorder()

OpenCV copyMakeBorder() 圖像邊界擴展詳解與實戰在圖像處理和計算機視覺中&#xff0c;有時需要在原始圖像的四周增加邊界&#xff08;Padding&#xff09;。這種操作在很多場景中都有應用&#xff0c;比如&#xff1a;卷積神經網絡&#xff08;CNN&#xff09;中的圖像預處理…

ansible管理變量和事實

ansible管理變量和事實與實施任務控制 在 Ansible 中&#xff0c;變量和事實&#xff08;Facts&#xff09;就像給劇本&#xff08;Playbook&#xff09;配備的 “信息工具箱”&#xff0c;讓你的自動化配置管理更靈活、更智能。 變量&#xff1a;提前準備的 “預設信息” 變…

STM32--寄存器與標準庫函數--基本定時器

目錄 前言 基本定時器概念 定時時間 定時器時鐘確定 倍頻鎖相環被正確配置為168MHz 定時器的庫函數操作 代碼 定時器的寄存器操作 代碼 寄存器 后言 前言 使用平臺:STM32F407ZET6 使用數據手冊&#xff1a;STM32F407數據手冊.pdf 使用參考手冊&…

PCA 實現多向量壓縮:首個主成分的深層意義

PCA 實現多向量壓縮 第一個主成分(components_[0])正是數據協方差矩陣中最大特征值對應的特征向量。 ,layer_attention_vector[layer] 被賦值為 pca.components_[0],這確實是一個特征向量,具體來說是 PCA 分解得到的第一個主成分(主特征向量)。 關于它的維度: 假設 c…

網絡常識-DNS如何解析

DNS&#xff08;Domain Name System&#xff0c;域名系統&#xff09;是互聯網的“地址簿”&#xff0c;負責將人類易記的域名&#xff08;如www.example.com&#xff09;轉換為計算機可識別的IP地址&#xff08;如192.168.1.1&#xff09;。其工作流程可以簡單理解為“從域名到…

Java中 23 種設計模式介紹,附帶示例

文章目錄設計模式六大原則設計模式分類1、創建型模式&#xff08;Creational Patterns&#xff09;2、結構型模式&#xff08;Structural Patterns&#xff09;3、行為型模式&#xff08;Behavioral Patterns&#xff09;一、創建型模式&#xff08;Creational Patterns&#x…

嵌入式開發入門——電子元器件~電磁繼電器、蜂鳴器

文章目錄電磁繼電器定義關鍵參數實物蜂鳴器實物內部結構分類關鍵參數電磁繼電器 定義 概述&#xff1a;電磁繼電器是利用電磁感應原理職稱的一種電磁開關&#xff0c;他能通過&#xff1a;低電壓、低電流的電路&#xff0c;來控制高電壓、高電流的電路。 關鍵參數 線圈電壓…

ROS2基礎

1.helloworld案例1.創建功能包&#xff08;C&#xff09;終端下&#xff0c;進入ws00_helloworld/src目錄&#xff0c;使用如下指令創建一個C 功能包:ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld執行完畢&a…

Python爬蟲實戰:研究pygalmesh,構建Thingiverse平臺三維網格數據處理系統

1. 引言 1.1 研究背景 在數字化浪潮席卷全球的當下,三維建模技術已成為連接虛擬與現實的核心紐帶,廣泛滲透于工程設計、地理信息系統(GIS)、虛擬現實(VR)、增強現實(AR)、醫學影像等關鍵領域。例如,在建筑工程中,BIM(建筑信息模型)技術依賴高精度三維網格實現施工…

開發者說 | EmbodiedGen:為具身智能打造可交互3D世界生成引擎

概述 具身智能的發展離不開高質量、多樣化的可交互3D仿真環境。為突破傳統構建方式的瓶頸&#xff0c;我們提出了EmbodiedGen&#xff0c;一個基于生成式AI技術的自動化3D世界生成引擎&#xff0c;助力低成本、高效率地創建真實且可交互的3D場景。用戶僅需輸入任務定義或場景圖…

GitHub Copilot:AI編程助手的架構演進與真實世界影響

本文由「大千AI助手」原創發布&#xff0c;專注用真話講AI&#xff0c;回歸技術本質。拒絕神話或妖魔化。搜索「大千AI助手」關注我&#xff0c;一起撕掉過度包裝&#xff0c;學習真實的AI技術&#xff01; 1. 技術背景與核心架構 GitHub Copilot 是由 GitHub 與 OpenAI 聯合開…

PDF OCR + 大模型:讓文檔理解不止停留在識字

在企業數字化的實際場景中&#xff0c;PDF OCR 已經很普遍了&#xff1a;從掃描件提取文本、表格到生成可搜索 PDF。但這類技術往往停留在"把圖片變成文字"&#xff0c;對文檔背后的語義、邏輯、業務價值理解不足。 而當 OCR 遇上大語言模型&#xff08;LLM&#xff…

半敏捷衛星觀測調度系統的設計與實現

半敏捷衛星觀測調度系統的設計與實現 摘要 本文詳細闡述了一個基于Python的半敏捷衛星觀測調度系統的設計與實現過程。系統針對半敏捷衛星特有的機動能力限制&#xff0c;綜合考慮了地面目標觀測需求、衛星資源約束、能源管理等多重因素&#xff0c;提出了一種混合啟發式算法解…

軟件測試中,常用的抓包工具有哪些?抓包的原理是什么?

回答重點在軟件測試中&#xff0c;常用的抓包工具主要有&#xff1a;1&#xff09;Fiddler2&#xff09;Wireshark3&#xff09;Charles4&#xff09;Postman&#xff08;它的攔截器功能也可以用于抓包&#xff09;5&#xff09;tcpdump抓包的原理大致是通過安裝在本地的抓包工…

Cesium學習(二)-地形可視化處理

Cesium地形可視化是其核心功能之一&#xff0c;允許開發者在3D地球中展示真實的地形數據。以下是關于Cesium地形可視化的詳細處理方法&#xff1a; 文章目錄1. 啟用地形可視化基本地形加載自定義地形提供者2. 地形相關操作地形高度采樣地形夸張效果3. 地形可視化設置地形照明效…

《告別 if-else 迷宮:Python 策略模式 (Strategy Pattern) 的優雅之道》

《告別 if-else 迷宮:Python 策略模式 (Strategy Pattern) 的優雅之道》 大家好,我是你的朋友,一位與 Python 代碼相伴多年的開發者。在我們的編程生涯中,幾乎都曾與一種“代碼怪獸”搏斗過,它就是那冗長、復雜、牽一發而動全身的 if-elif-else 結構。 每當一個新的需求…

Redis--day7--黑馬點評--優惠券秒殺

&#xff08;以下內容全部來自上述課程&#xff09;優惠券秒殺 1. 全局唯一ID 每個店鋪都可以發布優惠券:當用戶搶購時&#xff0c;就會生成訂單并保存到tb voucher order這張表中&#xff0c;而訂單表如果使用數據庫自增ID就存在一些問題: id的規律性太明顯受單表數據量的限制…