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