Android shader编译原理

news2024/10/6 18:25:22

作者:tmaczhang

1. 什么是着色器编译卡顿?

着色器是在 GPU(图形处理单元)上运行的代码。当 Flutter 渲染的 Skia 图形后端首次看到新的绘制命令序列时,它有时会生成和编译一个自定义的 GPU 着色器用于该命令序列。使得该序列和潜在类似的序列能够尽可能快地渲染。

然而不幸的是,Skia 着色器生成和编译的过程与帧的工作是依次进行的。编译过程可能需要几百毫秒的时间,而对于 60 帧/秒 (frame-per-second) 的显示来说,一个流畅的帧必须在 16 毫秒内绘制完成。因此,编译过程可能导致数十帧被丢失,使帧数从 60 降到 6。这就是所谓的 编译卡顿 。编译完成之后,动画应该会变得流畅。

另一方面,Impeller 在我们构建 Flutter 引擎时已经生成并编译了所有必要的着色器。因此,在 Impeller 上运行的应用程序已经拥有了它们所需的所有着色器,并且这些着色器不会在动画中引起卡顿。

要获得更加确切的着色器编译卡顿存在的证据,你可以在 --trace-skia 开启时查看追踪文件中的 GrGLProgramBuilder::finalize。下面的截图展示了一个 timeline 追踪的样例。

如何使用 SkSL 预热

在 1.20 发布的时候,Flutter 为应用开发者提供了一个命令行工具以收集终端用户在 SkSL(Skia 着色器语言)进行格式化处理中需要用到的着色器。 SkSL 着色器可以被打包进应用,并提前进行预热(预编译),这样当终端用户第一次打开应用时,就能够减少动画的编译掉帧了。

在flutter中,通过将SKSL着色器打包进应用,并且提前进行预编译,用空间换取时间,来提升性能,那么在android里是不是同样可以呢?

2 Android中的Shader编译和使用

2.1 shader原始逻辑

/data/user_de/0/tv.danmaku.bili/code_cache # ls -l
total 56
-r-------- 1 u0_a206 u0_a206_cache 40556 2023-06-30 15:32 com.android.opengl.shaders_cache
-r-------- 1 u0_a206 u0_a206_cache 13304 2023-06-30 15:32 com.android.skia.shaders_cache

frameworks/base/graphics/java/android/graphics/HardwareRenderer.java

/**
 * Name of the file that holds the shaders cache.
 */
private static final String CACHE_PATH_SHADERS = "com.android.opengl.shaders_cache";
private static final String CACHE_PATH_SKIASHADERS = "com.android.skia.shaders_cache";



/**
 * Sets the directory to use as a persistent storage for threaded rendering
 * resources.
 *
 * @param cacheDir A directory the current process can write to
 * @hide
 */
public static void setupDiskCache(File cacheDir) {
    setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath(),
            new File(cacheDir, CACHE_PATH_SKIASHADERS).getAbsolutePath());
}

static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, jobject clazz,
        jstring diskCachePath, jstring skiaDiskCachePath) {
    const char* cacheArray = env->GetStringUTFChars(diskCachePath, NULL);
    android::egl_set_cache_filename(cacheArray);
    env->ReleaseStringUTFChars(diskCachePath, cacheArray);

    const char* skiaCacheArray = env->GetStringUTFChars(skiaDiskCachePath, NULL);
    uirenderer::skiapipeline::ShaderCache::get().setFilename(skiaCacheArray);
    env->ReleaseStringUTFChars(skiaDiskCachePath, skiaCacheArray);
}

2.2 Skia介绍

在Render线程初始化的时候,会初始化路径,并且设置到native里。那么是怎么保存的呢?这就要介绍今天的主角SKia库。

android路径位于 external/skia/

官方描述:SkSL是Skia的着色语言。SkRuntimeEffect是一个Skia C++对象,可用于创建行为由SkSL代码控制的SkShader、SkColorFilter和SkBlender对象。 您可以在上试用SkSLhttps://shaders.skia.org/.语法与GLSL非常相似。在您的滑雪应用程序中使用SkSL效果时,需要记住(与GLSL的)重要差异。这些差异大多是因为一个基本事实:使用GPU着色语言,您正在编程GPU管道的一个阶段。使用SkSL,您正在对Skia管道的一个阶段进行编程。

float f(vec3 p) {
    p.z -= iTime * 10.;
    float a = p.z * .1;
    p.xy *= mat2(cos(a), sin(a), -sin(a), cos(a));
    return .1 - length(cos(p.xy) + sin(p.yz));
}

half4 main(vec2 fragcoord) { 
    vec3 d = .5 - fragcoord.xy1 / iResolution.y;
    vec3 p=vec3(0);
    for (int i = 0; i < 32; i++) {
      p += f(p) * d;
    }
    return ((sin(p) + vec3(2, 5, 9)) / length(p)).xyz1;
}

Shader和Program是两个重要的概念,至少需要创建一个顶点Shader对象、一个片段Shader对象和一个Program对象,才能用着色器进行渲染,理解Shader对象和Program对象的最佳方式是将它们比作C语言的编译器和链接程序,从Shader的创建到Program的链接共六个基本步骤,创建Shader、加载Shader源码、编译Shader、创建Program、绑定Program与Shader、链接Program。然后才能正常使用。

在Android中,shader被编译链接后,最后就存在了上面的目录下。

2.3 编译链接流程

在应用启动时候,

external/skia/src/gpu/gl/builders/GrGLProgramBuilder.cpp

void GrGLProgramBuilder::storeShaderInCache(const SkSL::Program::Inputs& inputs, GrGLuint programID,
                                            const std::string shaders[], bool isSkSL,
                                            SkSL::Program::Settings* settings) {
    if (!this->gpu()->getContext()->priv().getPersistentCache()) {
        return;
    }
    sk_sp<SkData> key = SkData::MakeWithoutCopy(this->desc().asKey(), this->desc().keyLength());
    SkString description = GrProgramDesc::Describe(fProgramInfo, *fGpu->caps());
    if (fGpu->glCaps().programBinarySupport()) {
        // binary cache
        GrGLsizei length = 0;
        GL_CALL(GetProgramiv(programID, GL_PROGRAM_BINARY_LENGTH, &length));
        if (length > 0) {
            SkBinaryWriteBuffer writer;
            writer.writeInt(GrPersistentCacheUtils::GetCurrentVersion());
            writer.writeUInt(kGLPB_Tag);

            writer.writePad32(&inputs, sizeof(inputs));

            SkAutoSMalloc<2048> binary(length);
            GrGLenum binaryFormat;
            GL_CALL(GetProgramBinary(programID, length, &length, &binaryFormat, binary.get()));

            writer.writeUInt(binaryFormat);
            writer.writeInt(length);
            writer.writePad32(binary.get(), length);

            auto data = writer.snapshotAsData();
            this->gpu()->getContext()->priv().getPersistentCache()->store(*key, *data, description);
        }
    } else {
        // source cache, plus metadata to allow for a complete precompile
        GrPersistentCacheUtils::ShaderMetadata meta;
        meta.fSettings = settings;
        meta.fHasCustomColorOutput = fFS.hasCustomColorOutput();
        meta.fHasSecondaryColorOutput = fFS.hasSecondaryOutput();
        for (auto attr : this->geometryProcessor().vertexAttributes()) {
            meta.fAttributeNames.emplace_back(attr.name());
        }
        for (auto attr : this->geometryProcessor().instanceAttributes()) {
            meta.fAttributeNames.emplace_back(attr.name());
        }

        auto data = GrPersistentCacheUtils::PackCachedShaders(isSkSL ? kSKSL_Tag : kGLSL_Tag,
                                                              shaders, &inputs, 1, &meta);
        this->gpu()->getContext()->priv().getPersistentCache()->store(*key, *data, description);
    }
}

注意这里 两种存储格式,前面是存储SKSL编译好的二进制文件,后面是存储SKSL源码

frameworks/base/libs/hwui/pipeline/skia/ShaderCache.cpp

void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) {
    ATRACE_NAME("ShaderCache::store");
    std::lock_guard<std::mutex> lock(mMutex);
    mNumShadersCachedInRam++;
    ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam);

    if (!mInitialized) {
        return;
    }

    size_t valueSize = data.size();
    size_t keySize = key.size();
    if (keySize == 0 || valueSize == 0 || valueSize >= maxValueSize) {
        ALOGW("ShaderCache::store: sizes %d %d not allowed", (int)keySize, (int)valueSize);
        return;
    }

    const void* value = data.data();

    BlobCache* bc = getBlobCacheLocked();
    if (mInStoreVkPipelineInProgress) {
        if (mOldPipelineCacheSize == -1) {
            // Record the initial pipeline cache size stored in the file.
            mOldPipelineCacheSize = bc->get(key.data(), keySize, nullptr, 0);
        }
        if (mNewPipelineCacheSize != -1 && mNewPipelineCacheSize == valueSize) {
            // There has not been change in pipeline cache size. Stop trying to save.
            mTryToStorePipelineCache = false;
            return;
        }
        mNewPipelineCacheSize = valueSize;
    } else {
        mCacheDirty = true;
        // If there are new shaders compiled, we probably have new pipeline state too.
        // Store pipeline cache on the next flush.
        mNewPipelineCacheSize = -1;
        mTryToStorePipelineCache = true;
    }
    set(bc, key.data(), keySize, value, valueSize);

    if (!mSavePending && mDeferredSaveDelayMs > 0) {
        mSavePending = true;
        std::thread deferredSaveThread([this]() {
            usleep(mDeferredSaveDelayMs * 1000);  // milliseconds to microseconds
            std::lock_guard<std::mutex> lock(mMutex);
            // Store file on disk if there a new shader or Vulkan pipeline cache size changed.
            if (mCacheDirty || mNewPipelineCacheSize != mOldPipelineCacheSize) {
                saveToDiskLocked();
                mOldPipelineCacheSize = mNewPipelineCacheSize;
                mTryToStorePipelineCache = false;
                mCacheDirty = false;
            }
            mSavePending = false;
        });
        deferredSaveThread.detach();
    }
}

最后通过saveToDiskLocked 保存到本地路径,也就是data/user_de/0/${packagename}/code_cache/com.android.skia.shaders_cache

frameworks/native/opengl/libs/EGL/FileBlobCache.cpp

void FileBlobCache::writeToFile() {
    if (mFilename.length() > 0) {
        size_t cacheSize = getFlattenedSize();
        size_t headerSize = cacheFileHeaderSize;
        const char* fname = mFilename.c_str();

        // Try to create the file with no permissions so we can write it
        // without anyone trying to read it.
        int fd = open(fname, O_CREAT | O_EXCL | O_RDWR, 0);
        if (fd == -1) {
            if (errno == EEXIST) {
                // The file exists, delete it and try again.
                if (unlink(fname) == -1) {
                    // No point in retrying if the unlink failed.
                    ALOGE("error unlinking cache file %s: %s (%d)", fname,
                            strerror(errno), errno);
                    return;
                }
                // Retry now that we've unlinked the file.
                fd = open(fname, O_CREAT | O_EXCL | O_RDWR, 0);
            }
            if (fd == -1) {
                ALOGE("error creating cache file %s: %s (%d)", fname,
                        strerror(errno), errno);
                return;
            }
        }

        size_t fileSize = headerSize + cacheSize;

        uint8_t* buf = new uint8_t [fileSize];
        if (!buf) {
            ALOGE("error allocating buffer for cache contents: %s (%d)",
                    strerror(errno), errno);
            close(fd);
            unlink(fname);
            return;
        }

        int err = flatten(buf + headerSize, cacheSize);
        if (err < 0) {
            ALOGE("error writing cache contents: %s (%d)", strerror(-err),
                    -err);
            delete [] buf;
            close(fd);
            unlink(fname);
            return;
        }

        // Write the file magic and CRC
        memcpy(buf, cacheFileMagic, 4);
        uint32_t* crc = reinterpret_cast<uint32_t*>(buf + 4);
        *crc = crc32c(buf + headerSize, cacheSize);

        if (write(fd, buf, fileSize) == -1) {
            ALOGE("error writing cache file: %s (%d)", strerror(errno),
                    errno);
            delete [] buf;
            close(fd);
            unlink(fname);
            return;
        }

        delete [] buf;
        fchmod(fd, S_IRUSR);
        close(fd);
    }
}

最后通过FileBlobCache的writeToFile写入到文件中。

2.4 shader文件使用原理

在Render线程创建的时候,会将shader文件读进内存。然后在应用加载图形的时候,在创建Program的时候通过这个 builder.fCached = persistentCache->load(*key) 从shader中查询,查询到了,后面就 不会执行绑定Program与Shader、链接Program了,从而到达了空间换时间的逻辑。

frameworks/native/opengl/libs/EGL/FileBlobCache.cpp

sk_sp<GrGLProgram> GrGLProgramBuilder::CreateProgram(
                                               GrDirectContext* dContext,
                                               const GrProgramDesc& desc,
                                               const GrProgramInfo& programInfo,
                                               const GrGLPrecompiledProgram* precompiledProgram) {
    TRACE_EVENT0_ALWAYS("skia.shaders", "shader_compile");
    GrAutoLocaleSetter als("C");

    GrGLGpu* glGpu = static_cast<GrGLGpu*>(dContext->priv().getGpu());

    // create a builder.  This will be handed off to effects so they can use it to add
    // uniforms, varyings, textures, etc
    GrGLProgramBuilder builder(glGpu, desc, programInfo);

    auto persistentCache = dContext->priv().getPersistentCache();
    if (persistentCache && !precompiledProgram) {
        sk_sp<SkData> key = SkData::MakeWithoutCopy(desc.asKey(), desc.keyLength());
        builder.fCached = persistentCache->load(*key);
        // the eventual end goal is to completely skip emitAndInstallProcs on a cache hit, but it's
        // doing necessary setup in addition to generating the SkSL code. Currently we are only able
        // to skip the SkSL->GLSL step on a cache hit.
    }
    if (!builder.emitAndInstallProcs()) {
        return nullptr;
    }
    return builder.finalize(precompiledProgram);
}




sk_sp<GrGLProgram> GrGLProgramBuilder::finalize(const GrGLPrecompiledProgram* precompiledProgram) {
    TRACE_EVENT0("skia.shaders", TRACE_FUNC);
    //省略逻辑
    bool cached = fCached.get() != nullptr;
    if (precompiledProgram) {
      //省略逻辑
    } else if (cached) {
        TRACE_EVENT0_ALWAYS("skia.shaders", "cache_hit");
        SkReadBuffer reader(fCached->data(), fCached->size());
    //省略逻辑
    }
//省略逻辑
}

3 Android中shader预加载技术

我们前面介绍了,shader文件是在第一次使用的时候创建的,那么第一次使用必然有编译链接等6个过程,也就是trace上面展示的,那么会导致render线程耗时过多,从而导致卡顿。

前面介绍了Flutter可以提前缓存SKSL打包到APK中,然后预加载,从而减少卡顿,是不是Android也可以这么做呢?

答案显然是可以的,看下flutter是怎么做的?

size_t PersistentCache::PrecompileKnownSkSLs(GrDirectContext* context) const {
  // clang-tidy has trouble reasoning about some of the complicated array and
  // pointer-arithmetic code in rapidjson.
  // NOLINTNEXTLINE(clang-analyzer-cplusplus.PlacementNew)
  auto known_sksls = LoadSkSLs();
  // A trace must be present even if no precompilations have been completed.
  FML_TRACE_EVENT("flutter", "PersistentCache::PrecompileKnownSkSLs", "count",
                  known_sksls.size());

  if (context == nullptr) {
    return 0;
  }

  size_t precompiled_count = 0;
  for (const auto& sksl : known_sksls) {
    TRACE_EVENT0("flutter", "PrecompilingSkSL");
    if (context->precompileShader(*sksl.key, *sksl.value)) {
      precompiled_count++;
    }
  }

  FML_TRACE_COUNTER("flutter", "PersistentCache::PrecompiledSkSLs",
                    reinterpret_cast<int64_t>(this),  // Trace Counter ID
                    "Successful", precompiled_count);
  return precompiled_count;
}

(1)收集SKSL shader文件。源码? 还是二进制呢?这个问题留给读者

(2)打包到APK中。

(3)初始化时候,将SKSL调用shader预编译接口加载到内存中,并且保存到本地。

如果你想更好的掌握性能优化相关问题,可以通过下方的学习文档进行参考学习,大家可以直接去https://qr18.cn/FVlo89访问查阅完整版


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/725270.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA对象转xml(支持递归生成复杂数据类型)

前言 调用一些soap协议的项目你或许使用的到&#xff0c;也许我是在造轮子&#xff0c;但是我没在网上找到合适的轮子&#xff0c;就根据现有的项目自己造了一个&#xff0c;废话不说&#xff0c;说思路 使用反射获取对象的属性&#xff0c;根据属性的类型做出相应的处理&…

计算机体系结构基础知识介绍之缓存性能的十大进阶优化之编译器控制的预取和利用HBM扩展内存层次(七)

优化九&#xff1a;编译器控制的预取以减少丢失惩罚或丢失率 硬件预取的替代方案是编译器在处理器需要数据之前插入预取指令来请求数据。 预取有两种类型&#xff1a; ■ 寄存器预取将值加载到寄存器中。 ■ 高速缓存预取仅将数据加载到高速缓存。 这两种类型都可以分为有错…

k8s对象操作的了解

一&#xff1a;什么是Kubernetes对象 Kubernetes对象指的是Kubernetes系统的持久化实体&#xff0c;所有这些对象合起来&#xff0c;代表了你集群的实际情况。常规的应用里&#xff0c;我们把应用程序的数据存储在数据库中&#xff0c;Kubernetes将其数据以Kubernetes对象的形…

unity+pico neo3入门教程

安装unity&#xff0c;教程如下&#xff1a;unity2021安装教程 安装pico的SDK:: https://developer-cn.pico-interactive.com/ 有入门教程&#xff1a;导入 SDK - PICO 开发者平台 注册后组织&#xff0c;创建应用learntest&#xff0c;如下 下载SDK。下载最新版&#xff…

旧固态硬盘复制到新固态硬盘多出一个分区怎么办?

朋友电脑只有512G而且只有一个硬盘口&#xff0c;然后就买了一款1T硬盘&#xff0c;去店里回来之后发现多出一个分区&#xff0c;无法直接在系统里合并 这时候我们就需要下载第三方软件&#xff0c;删除多余分区&#xff0c;扩展C盘 软件下载链接&#xff1a; DiskGenius.rar…

Stable Diffusion 用插件管理NNN个模型

当初步涉足 Stable Diffusion&#xff0c;可能会被各种新概念和模型搞得头大。好比我们作为新晋的魔法师&#xff0c;需要理解如何巧妙使用各种法师装备——也就是这些模型&#xff0c;以更好地应对问题&#xff0c;发挥出最大效果。 要了解一个被称为 safetensors 的概念。这…

TRACE请求造成XSS

漏洞描述 远端WWW服务支持TRACE请求。RFC 2616介绍了TRACE请求&#xff0c;该请求典型地用于测试HTTP协议实现。 漏洞危害 攻击者利用TRACE请求&#xff0c;结合其它浏览器端漏洞&#xff0c;有可能进行跨站脚本攻击&#xff0c;获取敏感信息&#xff0c;比如cookie中的认证信…

如何用思维导图规划旅行

又到了一年一度的旅游旺季&#xff0c;旅游是一件令人愉悦又放松的事情。合理的规划旅游不仅可以保证旅行的顺利进行。还可以帮助我们提前了解旅游地的信息&#xff0c;比如当地的民俗、文化、天气等等。特别的正值旅游旺季的时候&#xff0c;一份合理的规划&#xff0c;可以帮…

【IMX6ULL - LOGO替换】uboot阶段替换打印的开发板Logo教程

替换Board: I.MX6U VSTC,将其显示为Board: I.MX6U ALIENTEK (1)修改 uboot/board/freescale/mx6ullevk 路径下的 mx6ullevk.c 文件内容: int checkboard(void) {if (is_mx6ull_9x9_evk())puts(

ARM_串口解析器

include/serial.h #ifndef __UART4_H__ #define __UART4_H__#include "stm32mp1xx_rcc.h" #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_uart.h"//引脚封装 #define GPIO_PIN_0 0 #define GPIO_PIN_1 1 #define GPIO_PIN_2 2 #define GP…

当你知道前后端分离与不分离的6个特点,你就不该再当点工了!

前后端不分离 在早期&#xff0c;Web 应用开发主要采用前后端不分离的方式&#xff0c;它是以后端直接渲染模板完成响应为主的一种开发模式。以前后端不分离方式开发的 Web 应用的架构图如下&#xff1a; 浏览器向服务器发起请求&#xff0c;服务器接收到请求后去数据库中获取…

大数据之数据采集项目延伸——sqoop

承接上篇文章 大数据之数据采集项目总结——hadoop&#xff0c;hive&#xff0c;openresty&#xff0c;frcp&#xff0c;nginx&#xff0c;flume https://blog.csdn.net/qq_43759478/article/details/131520375?spm1001.2014.3001.5501 在上个阶段&#xff1a;完成了数据收集&…

查找满足条件的文件

linux 系统下查询当前多个子目录下满足条件的文件 find ./ -mindepth 2 -name *.png

attention中为啥multi-head输出结果进行concat,得到x,x还要乘上一个WO矩阵?

刚刚在敲vit模型代码&#xff0c;突然一个疑问&#xff0c;就是multi-head输出结果进行concat&#xff0c;得到x&#xff0c;x的维度是预期维度&#xff0c;然后再乘以一个WO矩阵&#xff0c;为啥要乘上一个WO矩阵&#xff0c;x的维度已经是预期的了&#xff1f;&#xff1f;&a…

C#基础学习_类的方法

C#基础学习_类的方法 概念:描述对象的动态特征 类型:实例方法、静态方法等 方法的定义: 访问修饰符(默认为private) 返回值类型 方法名(类型 参数1,类型 参数2,...) {//这里编写方法的主体(功能实现的具体过程)return 返回值; //若没有返回值,则不需要写该语句 }

2023年第四届“华数杯”全国大学生数学建模竞赛(附历年赛题和论文)

目录 华数杯简介大赛资料获取方式 华数杯简介 国赛前的预热”华数杯“第四届正在报名中&#xff0c;看到咨询我们的同学不少&#xff0c;挺多同学都非常感兴趣&#xff0c;但是又不清楚比赛的相关情况&#xff0c;这里将会给同学们一一答疑。 比赛难度&#xff1a;难度适中&am…

【面试常见】JS继承与原型、原型链

前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 在 JavaScript 中&#xff0c;继承是实现代码复用和构建对象关系的重要概念。本文将讨论原型链继承、构造函数继承以及…

QML Canvas 制作动画

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 终于要介绍动画了,这意味着我们快要把 Canvas 学完了,所以是时候庆祝一下了… 要在 Canvas 上实现动画,需要间隔一定的时间重绘动画的下一帧,而且频率要足够快,这样才能在图像切换时看起来像动画一样。…

一篇文章让你搞懂字符指针,数组指针,指针数组,数组传参和指针传参,函数指针

回顾 首先我们来大概回顾一下指针初阶的知识 内存会划分为一个个的内存单元&#xff0c;每个内存单元都有一个独立的编号—编号也被称为地址&#xff0c;地址在C语言中也被称为指针&#xff0c;指针&#xff08;地址&#xff09;需要存储起来—存储到变量中&#xff0c;这个变…

【Redis】缓存穿透、缓存击穿、缓存雪崩的原因及解决方案

文章目录 一、缓存穿透1.1 产生原因1.2 解决方法接口校验对空值进行缓存使用布隆过滤器实时监控 二、缓存雪崩2.2 解决方法将失效时间分散开给业务添加多级缓存构建缓存高可用集群使用锁或者队列的方式设置缓存标记 三、缓存击穿3.2 解决方法使用互斥锁”提前“使用互斥锁 / 逻…