Android shader编译原理

作者: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访问查阅完整版


猜你喜欢

转载自blog.csdn.net/weixin_61845324/article/details/131494349