<span class=“js_title_inner“>揭开Android Vulkan渲染封印:帧率暴增的底层指令</span>

Android 的自定义硬件加速(OpenGL 和 Vulkan) OpenGL 和 Vulkan 是图形渲染的底层 API,通常用来开发游戏或图形密集型应用,但它们在传统的 Android 开发中较少被深入讨论。探索如何用这些技术优化图形性能和实现特殊的效果,往往可以帮助提升应用的表现。在 Android 开发中,虽然许多开发者主要关注的是应用的功能和界面,但有一些低层次的技术,比如OpenGL ES和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

Android GPU编程:OpenGL ES、Vulkan与RenderScript GPU编程是指利用图形处理器的并行计算能力来执行通用计算或图形渲染任务。与CPU不同,GPU拥有数千个核心,专为处理大量并行计算而设计,特别适合处理图形渲染、矩阵运算、图像处理等任务[2]。在Android平台上,GPU编程主要通过两种API实现:OpenGL ES和Vulkan。OpenGL ES是OpenGL的嵌入式版本,历史悠久且兼容性好;Vulkan是新一代跨平台图形API,提供了更底层的硬件访问和更高的性能。// CPU: 串行处理,少量核心i < n;i++) {// 逐个处理。 阅读详情

相关推荐

在Android上优化Vulkan性能和细节

你可以使用Vulkan API提供的函数来查询设备和扩展的信息,然后根据需求进行选择和配置。在Android平台上,使用Vulkan可以有效地利用硬件资源,提供更好的性能和图形效果。创建和销毁对象的过程是昂贵的,频繁地创建和销毁对象会降低性能。在Vulkan中,命令缓冲区是执行图形和计算命令的关键。这段代码创建了一个Vulkan实例,并查询了设备的数量和信息。这段代码展示了在绘制循环之外创建和销毁Vulkan对象的方法。在使用Vulkan绘制图像时,选择适当的图像格式和内存布局可以提高性能和图形质量。

JaDentenet的博客 922

Android NDK开发详解Vulkan 图形之使用Vulkan

原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用

hnjzfwy的博客 3051

win10上基于Android studio环境vulkan编译必备工具

在win10上编译vulkan源码的必备工具,在ubuntu上和windows上编译Android官网提供的源码也需要此工具,包含在win10上android studio编译vulkan的小白操作指南。

Android渲染--重温硬件加速下Vulkan篇

本文是硬件加速的下篇,主要介绍以Vulkan为渲染引擎的绘制原理

jh1988abc的专栏 3239

Oracle11g安装教程、配置实例、监听、客户端程序详解_Windows篇

Oracle11g安装教程、配置实例、监听、客户端程序详解_Windows篇 文章目录Oracle11g安装教程、配置实例、监听、客户端程序详解_Windows篇前言一、数据库的安装前准备,前提是没有安装过!!!二、仅安装数据库软件(数据库运行环境)三、添加数据库实例四、设置监听程序,让数据库可以通过网络访问五、为目标数据库设置网名总结 前言 如果需要安装包:联系我看到会给你链接,因为现在官网已经不提供10g,11g的安装包了,在网盘保存望悉知(回复不会很及时,下班看到会回复敬请谅解)。 提示:以下

zhx__的博客 5630

YOLOv8模型部署与测试:保姆级教程(含Ultralytics配置避坑指南)

本文提供了一份详尽的YOLOv8模型部署与测试指南,涵盖环境配置、模型加载、参数优化、批量处理及API服务封装等关键步骤。重点分享了Ultralytics框架在实际部署中的常见问题与解决方案,帮助开发者高效完成从训练到生产的过渡,确保模型在真实场景中稳定运行。

weixin_29038155的博客 293

Android NDK开发详解Vulkan 图形之Android 上的 Vulkan 验证层

原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用

hnjzfwy的博客 2037

Ubuntu 24.04.3 LTS 安装 OpenClaw 图文版

OpenClow安装指南

qq_17204557的博客 2548

android全局使用vulkan,Vulkan  |  Android 开源项目  |  Android Open Source Project

Android 支持 Vulkan,这是一套适用于高性能 3D 图形的低开销、跨平台 API。与 OpenGL ES (GLES) 一样,Vulkan 也提供用于在应用中创建高品质实时图形的工具。使用 Vulkan 的优势包括降低 CPU 开销以及支持 SPIR-V 二进制中间语言。系统芯片 (SoC) 供应商(例如 GPU 独立硬件供应商 (IHV))可以编写适用于 Android 的 Vulk...

weixin_30348079的博客 2873

RAG-Anything 实战指南:从零构建企业级多模态知识库

本文是RAG-Anything的实战指南,详细介绍了如何从零构建企业级多模态知识库。通过一体化框架,它能无差别解析文本、表格、图片、公式等多格式文档,并构建可深度理解和智能检索的知识图谱。指南涵盖了环境搭建、核心实战、智能查询及生产级部署优化,帮助企业将杂乱文档转化为智能知识大脑。

pandas7gardener的博客 242

诠视MR眼镜Unity开发设置_Unity2020.3.38

请按照如下设置Android SDK 和 Gradle。打包前先将Unity切换到Android模式。Preferences 设置。

m0_71932574的博客 599

揭开Android Vulkan渲染封印:帧率暴增的底层指令

这个函数里调用RenderProxy.cpp的syncAndDrawFrame()函数,syncAndDrawFrame()函数里调用DrawFrameTask.cpp的drawFrame()函数,此函数调用函数当前类的postAndWait()函数,然后函数里调用RenderThread的queue()函数,将当前Task入到渲染线程的队列;那么上文中的mRenderPipeline->draw()函数实际就是SkiaVulkanPipeline->draw()函数调用,draw()函数中调用到。

吴延宝 1062

非洲秃鹫优化算法(AVOA)详解:从原理到实战

非洲秃鹫优化算法(AVOA)是一种模拟秃鹫觅食行为的元启发式算法。该算法通过数学建模秃鹫的群体分工、饥饿感知和食物竞争等行为,将优化问题转化为智能觅食过程。AVOA包含四个关键阶段:(1)种群初始化与最佳秃鹫选择;(2)饥饿率控制探索与开发平衡;(3)探索阶段模拟远距离飞行搜索;(4)开发阶段模拟食物源附近的竞争行为。算法采用饥饿率函数实现自适应搜索,初期偏向全局探索,后期转向局部开发。实验表明AVOA在标准测试函数和工程设计问题上表现出优越性能,其创新性在于将秃鹫独特的生物学特性转化为高效的优化机制。

qq_42212808的博客 225

支持 vulkan android,Vulkan 使用入门

本文档将通过下载、编译和运行几个示例应用的方式,介绍如何开始使用 Vulkan 图形库。首先,请确保您已准备好合适的硬件和平台版本。您应使用支持 Vulkan 且搭载 Android API 级别 24 或更高级别的设备。如需确认您的 Android 版本,请转到设置菜单,然后依次选择关于 > Android 版本。确认您已设置合适的硬件和平台版本后,您便可以下载所需的软件。下载首先,您必...

weixin_36221113的博客 4570

088、多摄像头跨镜目标追踪 ID 频繁切换?ReID 特征 + 轨迹关联 + 时间约束方案

文章摘要:本文针对多摄像头跨镜目标追踪中ID频繁切换的问题,提出了一种结合ReID特征、轨迹关联和时间约束的综合解决方案。作者分析了ID切换的三个主要原因:ReID特征不稳定、轨迹关联依赖最近邻匹配以及忽略时间约束。通过改进ReID特征提取(多层特征融合)、引入轨迹预测(卡尔曼滤波)和添加时间窗口约束,成功将ID切换率从35%降至3%以下。文章详细分享了实现细节和踩坑经验,为跨摄像头目标追踪提供了实用优化思路。

ai技术增长的博客 169

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Android系统Vulkan开发指南:下一代图形API

随着移动设备图形处理需求的不断增长,传统的图形API在性能和效率上逐渐显现出局限性。Vulkan作为下一代图形API,以其低开销、多线程支持等特性,为Android系统上的图形开发带来了新的机遇。本指南的目的是帮助开发者深入了解Vulkan在Android系统上的开发流程和技术细节,范围涵盖从Vulkan的基本概念到实际项目开发的各个方面。本文将按照以下结构展开:首先介绍Vulkan的核心概念和联系,包括其原理和架构;接着讲解核心算法原理和具体操作步骤,并结合Python代码进行说明;

操作系统内核探秘的博客 1606

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

使用Vulkan技术在Android上提升游戏画质

随着移动设备性能的不断提升,游戏开发者们也在不断寻求新的方法来提升游戏的画质和性能。Vulkan作为一种现代的图形API,提供了更高效的硬件访问和更低的CPU开销,使其成为在Android平台上提升游戏画质的理想选择。本文将深入探讨如何在Android上使用Vulkan技术提升游戏画质,并分享一些关键代码示例。

2301_76781847的博客 3120

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Android Vulkan 官宣转正并统一渲染堆栈 ,这对 Flutter 又有什么影响?

**什么是 swapchain?** swapchain 简单说就是一种用于管理多个缓冲区的机制,从而确保平滑渲染和显示画面,进而防止画面撕裂,比如 swapchain 通常会有双缓冲或三缓冲,通过实现类似一个缓冲区显示的同时,另一个缓冲区正在准备渲染一下帧。通俗又不严谨的说法:现在的 GPU 渲染效率很高,而系统显示的速度跟不上 GPU 渲染的速度,所有可以通过多重 buffer 的作用,提前在 GPU 渲染画面,等待提交,而提交给系统显示的过程中,就是在 buffer 之间进行交换 (Swap)

恋猫de小郭的博客 3873

android Vulkan 图形新特性研究

Vulkan 最早是由科纳斯组织(Khronos Group)在 2015 年的游戏开发者大会 (GDC)上提出来的,这是底层跨平台的 3D 图像及运算应用程序接口(API),其被 视作是 OpenGL 的后续产品。它可在不同的操作系统(比如 Windows、Linux 或 Android)上使用。Vulkan 与 OpenGL 之间有相似之处,包括图形管道阶段、OpenGL 着色器(一定程度上)

不是CC哥 1186

Android NDK开发详解Vulkan 图形之Android 上的 Vulkan 着色器编译器

原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用

hnjzfwy的博客 784

Android N(7.0)中的Vulkan支持

Vulkan为Khronos Group推出的下一代跨平台图形开发接口,用于替代历史悠久的OpenGL。Android从7.0(Nougat)开始加入了对其的支持。Vulkan与OpenGL相比,接口更底层,从而使开发者能更直接地控制GPU。由于更好的并行支持,及更小的开销,性能上也有一定的提升。另外层式架构可以帮助减少调试和测试的时间。但是,代价是实现相同的功能更复杂了。原本用OpenGL写个最简单的demo百来行,用vulkan祼写的话没千把行下不来。因此实际使用中需要有utility层来简化接口调用。

世事难料,保持低调 2万+

Vulkan Android 实战 - 接口导入

大家好,接下来将为大家介绍Android Vulkan 使用配置。 首先要确定手机是否支持Vulkan,可以下载一个AIDA64应用app,在设备中如果能找到vulkan设备,说明支持,否则不支持。 严格按照官方介绍的步骤一步步执行,就能获得官方推荐的可执行的例子,大家可以试一试。

sy_liao的专栏 3630

Vulkan学习--基于Andriod

Vulkan于2016年初发布至今,PC大多显卡更新后都 可以支持 移动平台现在骁龙820以及Mali的T860& T880 系列都已经支持 本文主要介绍基于Android的Vulkan环境设置以及运行相关demo 1. 系统要求  *支持vuklan的设备 1)下载Android studio 2)下载NDK r12-beta 3)下载Andoid N-preview SD

jaccen的专栏 5690

重棒:android7.0要从opengles 切到Vulkan ,气死程序员,那我们先来看看vulkan是什么吧

 原文链接,更多 狄八哥 点击链接加入群【tango.unity虚拟现实技术】:http://jq.qq.com/?_wv=1027&k=2Fnro1G 1. Khronos工业组织标准介绍 Khronos Group团队成立于2000年1月,由包括3Dlabs, ATI, Discreet, Evans & Sutherland, In

sinat_35801282的博客 2万+
上一篇: 揭开Android Vulkan渲染封印:帧率暴增的底层指令
下一篇: 为什么SurfaceFlinger是Android流畅显示的幕后英雄?合成过程详解
IT烂笔头
博客等级 码龄15年 621粉丝 78原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值