Store VkPipelineCache objects in PipelineCache
Implement PipelineCache, a new file-backed store for VkPipelineCache
objects. Migrate VkPipelineCache objects away from ShaderCache and store
them in PipelineCache instead.
Bug: 269117286
Flag: com.android.graphics.hwui.flags.separate_pipeline_cache
Test: adb root && adb shell aflags enable com.android.graphics.hwui.flags.separate_pipeline_cache && adb reboot
Test: atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk"
Test: boot to home, run settings app
Test: adb logcat | rg HWUI -C 10
Test: adb shell perfetto --time 10s --out /tmp/perfetto.trace view
Change-Id: Icc2fc14d4220a34269e5999491b64662a16ef3fd
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index 4f8044e..15d12e8 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -179,8 +179,9 @@
/**
* 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";
+ private static final String CACHE_PATH_OPENGL_SHADERS = "com.android.opengl.shaders_cache";
+ private static final String CACHE_PATH_SKIA_SHADERS = "com.android.skia.shaders_cache";
+ private static final String CACHE_PATH_SKIA_PIPELINES = "com.android.skia.pipelines_cache";
private static int sDensityDpi = 0;
@@ -1292,8 +1293,10 @@
* @hide
*/
public static void setupDiskCache(File cacheDir) {
- setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath(),
- new File(cacheDir, CACHE_PATH_SKIASHADERS).getAbsolutePath());
+ setupPersistentGraphicsCache(
+ new File(cacheDir, CACHE_PATH_OPENGL_SHADERS).getAbsolutePath(),
+ new File(cacheDir, CACHE_PATH_SKIA_SHADERS).getAbsolutePath(),
+ new File(cacheDir, CACHE_PATH_SKIA_PIPELINES).getAbsolutePath());
}
/** @hide */
@@ -1587,7 +1590,8 @@
protected static native boolean isWebViewOverlaysEnabled();
/** @hide */
- protected static native void setupShadersDiskCache(String cacheFile, String skiaCacheFile);
+ protected static native void setupPersistentGraphicsCache(
+ String openglShaderCachePath, String skiaShaderCachePath, String skiaPipelineCachePath);
private static native void nRotateProcessStatsBuffer();
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 2c11c84..0780ec2 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -664,6 +664,8 @@
"pipeline/skia/ATraceMemoryDump.cpp",
"pipeline/skia/GLFunctorDrawable.cpp",
"pipeline/skia/LayerDrawable.cpp",
+ "pipeline/skia/PersistentGraphicsCache.cpp",
+ "pipeline/skia/PipelineCache.cpp",
"pipeline/skia/ShaderCache.cpp",
"pipeline/skia/SkiaGpuPipeline.cpp",
"pipeline/skia/SkiaMemoryTracer.cpp",
@@ -814,6 +816,8 @@
"tests/unit/MatrixTests.cpp",
"tests/unit/OpBufferTests.cpp",
"tests/unit/PathInterpolatorTests.cpp",
+ "tests/unit/PersistentGraphicsCacheTests.cpp",
+ "tests/unit/PipelineCacheTests.cpp",
"tests/unit/RenderEffectCapabilityQueryTests.cpp",
"tests/unit/RenderNodeDrawableTests.cpp",
"tests/unit/RenderNodeTests.cpp",
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 9c83c59..6cc3d66 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -43,6 +43,7 @@
#include <media/NdkImageReader.h>
#include <nativehelper/JNIPlatformHelp.h>
#ifdef __ANDROID__
+#include <pipeline/skia/PersistentGraphicsCache.h>
#include <pipeline/skia/ShaderCache.h>
#include <private/EGL/cache.h>
#endif
@@ -945,19 +946,25 @@
}
// ----------------------------------------------------------------------------
-// Shaders
+// Persistent graphics cache
// ----------------------------------------------------------------------------
-static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, jobject clazz,
- jstring diskCachePath, jstring skiaDiskCachePath) {
+static void android_view_ThreadedRenderer_setupPersistentGraphicsCache(
+ JNIEnv* env, jobject clazz, jstring openglShaderCachePath, jstring skiaShaderCachePath,
+ jstring skiaPipelineCachePath) {
#ifdef __ANDROID__
- const char* cacheArray = env->GetStringUTFChars(diskCachePath, NULL);
- android::egl_set_cache_filename(cacheArray);
- env->ReleaseStringUTFChars(diskCachePath, cacheArray);
+ const char* openglShaderCachePathArray = env->GetStringUTFChars(openglShaderCachePath, NULL);
+ android::egl_set_cache_filename(openglShaderCachePathArray);
+ env->ReleaseStringUTFChars(openglShaderCachePath, openglShaderCachePathArray);
- const char* skiaCacheArray = env->GetStringUTFChars(skiaDiskCachePath, NULL);
- uirenderer::skiapipeline::ShaderCache::get().setFilename(skiaCacheArray);
- env->ReleaseStringUTFChars(skiaDiskCachePath, skiaCacheArray);
+ const char* skiaShaderCachePathArray = env->GetStringUTFChars(skiaShaderCachePath, NULL);
+ uirenderer::skiapipeline::ShaderCache::get().setFilename(skiaShaderCachePathArray);
+ env->ReleaseStringUTFChars(skiaShaderCachePath, skiaShaderCachePathArray);
+
+ const char* skiaPipelineCachePathArray = env->GetStringUTFChars(skiaPipelineCachePath, NULL);
+ uirenderer::skiapipeline::PersistentGraphicsCache::get().initPipelineCache(
+ skiaPipelineCachePathArray);
+ env->ReleaseStringUTFChars(skiaPipelineCachePath, skiaPipelineCachePathArray);
#endif
}
@@ -1025,8 +1032,9 @@
(void*)android_view_ThreadedRenderer_dumpProfileInfo},
{"nDumpGlobalProfileInfo", "(Ljava/io/FileDescriptor;I)V",
(void*)android_view_ThreadedRenderer_dumpGlobalProfileInfo},
- {"setupShadersDiskCache", "(Ljava/lang/String;Ljava/lang/String;)V",
- (void*)android_view_ThreadedRenderer_setupShadersDiskCache},
+ {"setupPersistentGraphicsCache",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
+ (void*)android_view_ThreadedRenderer_setupPersistentGraphicsCache},
{"nAddRenderNode", "(JJZ)V", (void*)android_view_ThreadedRenderer_addRenderNode},
{"nRemoveRenderNode", "(JJ)V", (void*)android_view_ThreadedRenderer_removeRenderNode},
{"nDrawRenderNode", "(JJ)V", (void*)android_view_ThreadedRendererd_drawRenderNode},
diff --git a/libs/hwui/pipeline/skia/PersistentGraphicsCache.cpp b/libs/hwui/pipeline/skia/PersistentGraphicsCache.cpp
new file mode 100644
index 0000000..c0d00eb
--- /dev/null
+++ b/libs/hwui/pipeline/skia/PersistentGraphicsCache.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PersistentGraphicsCache.h"
+
+#include <SkData.h>
+#include <SkRefCnt.h>
+#include <SkString.h>
+#include <ganesh/GrDirectContext.h>
+#include <log/log.h>
+
+#include <cstddef>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "Properties.h"
+#include "ShaderCache.h"
+
+#ifdef __linux__
+#include <com_android_graphics_hwui_flags.h>
+namespace hwui_flags = com::android::graphics::hwui::flags;
+#else // __linux__
+namespace hwui_flags {
+constexpr bool separate_pipeline_cache() {
+ return false;
+}
+} // namespace hwui_flags
+#endif // __linux__
+
+namespace {
+
+constexpr size_t kMaxPipelineSizeBytes = 2 * 1024 * 1024;
+
+} // namespace
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+PersistentGraphicsCache& PersistentGraphicsCache::get() {
+ static PersistentGraphicsCache cache;
+ return cache;
+}
+
+void PersistentGraphicsCache::initPipelineCache(std::string path,
+ useconds_t writeThrottleInterval) {
+ if (!hwui_flags::separate_pipeline_cache()) {
+ return;
+ }
+
+ mPipelineCache = std::make_unique<PipelineCache>(std::move(path), writeThrottleInterval);
+}
+
+void PersistentGraphicsCache::onVkFrameFlushed(GrDirectContext* context) {
+ class RealGrDirectContext : public GrDirectContextWrapper {
+ private:
+ GrDirectContext* mContext;
+
+ public:
+ RealGrDirectContext(GrDirectContext* context) : mContext(context) {}
+
+ bool canDetectNewVkPipelineCacheData() const override {
+ return mContext->canDetectNewVkPipelineCacheData();
+ }
+
+ bool hasNewVkPipelineCacheData() const override {
+ return mContext->hasNewVkPipelineCacheData();
+ }
+
+ void storeVkPipelineCacheData(size_t maxSize) override {
+ return mContext->storeVkPipelineCacheData(maxSize);
+ }
+
+ GrDirectContext* unwrap() const override { return mContext; }
+ };
+
+ RealGrDirectContext wrapper(context);
+ onVkFrameFlushed(&wrapper);
+}
+
+void PersistentGraphicsCache::onVkFrameFlushed(GrDirectContextWrapper* context) {
+ if (!hwui_flags::separate_pipeline_cache()) {
+ ShaderCache::get().onVkFrameFlushed(context->unwrap());
+ return;
+ }
+
+ mCanDetectNewVkPipelineCacheData = context->canDetectNewVkPipelineCacheData();
+ if (context->hasNewVkPipelineCacheData()) {
+ context->storeVkPipelineCacheData(kMaxPipelineSizeBytes);
+ }
+}
+
+sk_sp<SkData> PersistentGraphicsCache::load(const SkData& key) {
+ if (!hwui_flags::separate_pipeline_cache()) {
+ return ShaderCache::get().load(key);
+ }
+
+ if (mPipelineCache == nullptr) {
+ LOG_ALWAYS_FATAL(
+ "PersistentGraphicsCache::load: pipeline cache path was not initialized, aborting "
+ "load");
+ return nullptr;
+ }
+
+ auto data = mPipelineCache->tryLoad(key);
+ if (data != nullptr) {
+ return data;
+ }
+
+ return ShaderCache::get().load(key);
+}
+
+void PersistentGraphicsCache::store(const SkData& key, const SkData& data,
+ const SkString& description) {
+ if (!hwui_flags::separate_pipeline_cache()) {
+ ShaderCache::get().store(key, data, description);
+ return;
+ }
+
+ if (mPipelineCache == nullptr) {
+ LOG_ALWAYS_FATAL(
+ "PersistentGraphicsCache::store: pipeline cache path was not initialized, aborting "
+ "store");
+ return;
+ }
+
+ if (mPipelineCache->canStore(description)) {
+ if (mCanDetectNewVkPipelineCacheData) {
+ mPipelineCache->store(key, data);
+ } else if (mLastPipelineCacheSize != data.size()) {
+ mPipelineCache->store(key, data);
+ mLastPipelineCacheSize = data.size();
+ }
+ return;
+ }
+
+ ShaderCache::get().store(key, data, description);
+}
+
+} // namespace skiapipeline
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/pipeline/skia/PersistentGraphicsCache.h b/libs/hwui/pipeline/skia/PersistentGraphicsCache.h
new file mode 100644
index 0000000..385ce33
--- /dev/null
+++ b/libs/hwui/pipeline/skia/PersistentGraphicsCache.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <ganesh/GrContextOptions.h>
+#include <ganesh/GrDirectContext.h>
+#include <sys/types.h>
+
+#include <cstddef>
+#include <memory>
+#include <string>
+
+#include "PipelineCache.h"
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+// Delegate persistent cache operations to either the pipeline cache or the shader cache as
+// appropriate
+class PersistentGraphicsCache : public GrContextOptions::PersistentCache {
+ static constexpr useconds_t kDefaultWriteThrottleInterval = 4 * 1000 * 1000;
+
+public:
+ static PersistentGraphicsCache& get();
+
+ void initPipelineCache(std::string path,
+ useconds_t writeThrottleInterval = kDefaultWriteThrottleInterval);
+ void onVkFrameFlushed(GrDirectContext* context);
+
+ sk_sp<SkData> load(const SkData& key) override;
+ void store(const SkData& key, const SkData& data, const SkString& description) override;
+
+private:
+ std::unique_ptr<PipelineCache> mPipelineCache;
+
+ // Workarounds for devices without VK_EXT_pipeline_creation_cache_control
+ bool mCanDetectNewVkPipelineCacheData;
+ size_t mLastPipelineCacheSize;
+
+ // Unit test infrastructure
+ class GrDirectContextWrapper {
+ public:
+ virtual ~GrDirectContextWrapper() = default;
+
+ virtual bool canDetectNewVkPipelineCacheData() const = 0;
+ virtual bool hasNewVkPipelineCacheData() const = 0;
+ virtual void storeVkPipelineCacheData(size_t maxSize) = 0;
+
+ virtual GrDirectContext* unwrap() const = 0;
+ };
+
+ void onVkFrameFlushed(GrDirectContextWrapper* context);
+
+ friend class PersistentGraphicsCacheTestUtils;
+};
+
+} // namespace skiapipeline
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/pipeline/skia/PipelineCache.cpp b/libs/hwui/pipeline/skia/PipelineCache.cpp
new file mode 100644
index 0000000..3d2021d
--- /dev/null
+++ b/libs/hwui/pipeline/skia/PipelineCache.cpp
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PipelineCache.h"
+
+#include <SkData.h>
+#include <SkRefCnt.h>
+#include <SkString.h>
+#include <android-base/unique_fd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <log/log.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <utils/Trace.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <fstream>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace {
+
+using key_size_t = uint32_t;
+
+void releaseProc(const void* ptr, void* context) {
+ const auto size = reinterpret_cast<size_t>(context);
+ release(Memory{.data = const_cast<void*>(ptr), .size = size});
+}
+
+struct PipelineCacheData {
+ key_size_t keySize;
+ sk_sp<SkData> key;
+ sk_sp<SkData> data;
+
+ struct LoadResult {
+ enum Outcome {
+ Success,
+ CouldNotAcquire,
+ ZeroSize,
+ NoKeySize,
+ NoKey,
+ };
+ Outcome outcome;
+ AcquireResult acquireResult;
+ };
+
+ static LoadResult load(const std::string& path, PipelineCacheData& cache) {
+ Memory memory;
+ auto result = acquire(path, memory);
+ if (result.outcome != AcquireResult::Success) {
+ return LoadResult{.outcome = LoadResult::CouldNotAcquire, .acquireResult = result};
+ }
+
+ if (memory.size == 0) {
+ release(memory);
+ return LoadResult{.outcome = LoadResult::ZeroSize};
+ }
+
+ if (memory.size < sizeof(key_size_t)) {
+ release(memory);
+ return LoadResult{.outcome = LoadResult::NoKeySize};
+ }
+ memcpy(&cache.keySize, memory.data, sizeof(key_size_t));
+
+ if (memory.size < (sizeof(key_size_t) + cache.keySize)) {
+ release(memory);
+ return LoadResult{.outcome = LoadResult::NoKey};
+ }
+ cache.key = SkData::MakeWithCopy(static_cast<uint8_t*>(memory.data) + sizeof(key_size_t),
+ cache.keySize);
+
+ auto dataSize = memory.size - sizeof(key_size_t) - cache.keySize;
+ cache.data = SkData::MakeWithProc(
+ static_cast<uint8_t*>(memory.data) + sizeof(key_size_t) + cache.keySize, dataSize,
+ releaseProc, reinterpret_cast<void*>(memory.size));
+
+ return LoadResult{.outcome = LoadResult::Success};
+ }
+};
+
+void logLoadWarning(PipelineCacheData::LoadResult result, const char* message) {
+ if (result.outcome == PipelineCacheData::LoadResult::CouldNotAcquire) {
+ // Missing file is a normal case (cache was never written - there is no failure)
+ if ((result.acquireResult.outcome == AcquireResult::OpenFailed) &&
+ (result.acquireResult.errnoValue == ENOENT)) {
+ return;
+ }
+
+ ALOGW("%s; acquire outcome=%d, errnoValue=%d", message, result.acquireResult.outcome,
+ result.acquireResult.errnoValue);
+ return;
+ }
+
+ ALOGW("%s; outcome=%d", message, result.outcome);
+}
+
+} // namespace
+
+AcquireResult acquire(const std::string& path, Memory& memory) {
+ android::base::unique_fd fd(open(path.c_str(), O_RDONLY));
+ if (fd.get() == -1) {
+ return AcquireResult{.outcome = AcquireResult::OpenFailed, .errnoValue = errno};
+ }
+
+ struct stat stat;
+ auto result = fstat(fd.get(), &stat);
+ if (result == -1) {
+ return AcquireResult{.outcome = AcquireResult::FstatFailed, .errnoValue = errno};
+ }
+ if (stat.st_size == 0) {
+ return AcquireResult{.outcome = AcquireResult::CannotMmapZeroSizeFile};
+ }
+
+ auto data = mmap(nullptr, stat.st_size, PROT_READ, MAP_SHARED, fd.get(), 0);
+ if (data == reinterpret_cast<void*>(-1)) {
+ return AcquireResult{.outcome = AcquireResult::MmapFailed, .errnoValue = errno};
+ }
+
+ memory.data = data;
+ memory.size = stat.st_size;
+
+ return AcquireResult{.outcome = AcquireResult::Success};
+}
+
+ReleaseResult release(Memory memory) {
+ auto result = munmap(memory.data, memory.size);
+ if (result == -1) {
+ return ReleaseResult{.outcome = ReleaseResult::MunmapFailed, .errnoValue = errno};
+ }
+
+ return ReleaseResult{.outcome = ReleaseResult::Success};
+}
+
+PipelineCacheStore::PipelineCacheStore(useconds_t writeThrottleInterval)
+ : mWriteThrottleInterval(writeThrottleInterval)
+ , mMutex()
+ , mConditionVariable()
+ , mStoreRequest()
+ , mExit(false)
+ , mThread(&PipelineCacheStore::runThread, this) {}
+
+PipelineCacheStore::~PipelineCacheStore() {
+ mExit = true;
+ mConditionVariable.notify_one();
+ mThread.join();
+}
+
+void PipelineCacheStore::runThread() {
+ while (true) {
+ {
+ std::unique_lock<std::mutex> lock(mMutex);
+ mConditionVariable.wait(lock, [this] { return mExit || mStoreRequest.has_value(); });
+ }
+
+ if (mExit) {
+ return;
+ }
+
+ {
+ ATRACE_NAME("PipelineCacheStore::runThread (delay to throttle cache requests)");
+ // Frequent sequential cache writes will be written at most once per interval to reduce
+ // I/O activity.
+ usleep(mWriteThrottleInterval);
+ }
+
+ StoreRequest storeRequest;
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+
+ storeRequest = std::move(mStoreRequest.value());
+ mStoreRequest.reset();
+ }
+
+ {
+ ATRACE_NAME("PipelineCacheStore::runThread (write to file cache)");
+
+ android::base::unique_fd fd(creat(storeRequest.path.c_str(), S_IRUSR | S_IWUSR));
+ if (fd.get() == -1) {
+ ALOGE("PipelineCacheStore::runThread: could not open pipeline cache file (errno = "
+ "%d)",
+ errno);
+ continue;
+ }
+
+ auto written = write(fd.get(), storeRequest.data.data(), storeRequest.data.size());
+ if (written == -1) {
+ ALOGE("PipelineCacheStore::runThread: could not write to pipeline cache file "
+ "(errno = %d)",
+ errno);
+ continue;
+ }
+
+ ATRACE_INT64("HWUI pipeline cache size", written);
+ }
+ }
+}
+
+void PipelineCacheStore::store(std::string path, std::vector<uint8_t> data) {
+ ATRACE_NAME("PipelineCacheStore::store (lock mutex and notify condition)");
+
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mStoreRequest = StoreRequest{
+ .path = std::move(path),
+ .data = std::move(data),
+ };
+ }
+
+ mConditionVariable.notify_one();
+}
+
+PipelineCache::PipelineCache(std::string storePath, useconds_t writeThrottleInterval)
+ : mStorePath(std::move(storePath))
+ , mPipelineCacheStore(writeThrottleInterval)
+ , mKey(SkData::MakeEmpty())
+ , mData(SkData::MakeEmpty()) {
+ PipelineCacheData cache;
+ auto result = PipelineCacheData::load(mStorePath, cache);
+ if (result.outcome != PipelineCacheData::LoadResult::Success) {
+ logLoadWarning(
+ result,
+ "PipelineCache::PipelineCache: could not load cache key (cache will be dropped)");
+ return;
+ }
+
+ mKey = cache.key;
+ mData = cache.data;
+}
+
+sk_sp<SkData> PipelineCache::tryLoad(const SkData& key) {
+ ATRACE_NAME("PipelineCache::tryLoad");
+
+ if (!key.equals(mKey.get())) {
+ return nullptr;
+ }
+
+ if (mData == nullptr) {
+ ALOGW("PipelineCache::tryLoad: multiple data loads, incurring a load cost on the critical "
+ "path");
+
+ PipelineCacheData cache;
+ auto result = PipelineCacheData::load(mStorePath, cache);
+ if (result.outcome != PipelineCacheData::LoadResult::Success) {
+ logLoadWarning(
+ result,
+ "PipelineCache::tryLoad: could not load cache key (cache will be dropped)");
+ return nullptr;
+ }
+
+ return std::move(cache.data);
+ }
+
+ return std::move(mData);
+}
+
+bool PipelineCache::canStore(const SkString& description) const {
+ return description == SkString("VkPipelineCache");
+}
+
+void PipelineCache::store(const SkData& key, const SkData& data) {
+ ATRACE_NAME("PipelineCache::store");
+
+ mKey = SkData::MakeWithCopy(key.data(), key.size());
+
+ auto dataSize = sizeof(key_size_t) + key.size() + data.size();
+ std::vector<uint8_t> pendingData(dataSize);
+ auto ptr = pendingData.data();
+
+ auto keySize = static_cast<key_size_t>(key.size());
+ memcpy(ptr, &keySize, sizeof(key_size_t));
+ ptr += sizeof(key_size_t);
+
+ memcpy(ptr, key.data(), key.size());
+ ptr += key.size();
+
+ memcpy(ptr, data.data(), data.size());
+
+ mPipelineCacheStore.store(mStorePath, std::move(pendingData));
+}
diff --git a/libs/hwui/pipeline/skia/PipelineCache.h b/libs/hwui/pipeline/skia/PipelineCache.h
new file mode 100644
index 0000000..812523b
--- /dev/null
+++ b/libs/hwui/pipeline/skia/PipelineCache.h
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <SkData.h>
+#include <SkRefCnt.h>
+#include <SkString.h>
+#include <sys/types.h>
+
+#include <atomic>
+#include <condition_variable>
+#include <cstddef>
+#include <cstdint>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <thread>
+#include <vector>
+
+struct Memory {
+ void* data;
+ size_t size;
+};
+
+struct AcquireResult {
+ enum Outcome {
+ Success,
+ OpenFailed,
+ FstatFailed,
+ CannotMmapZeroSizeFile,
+ MmapFailed,
+ };
+ Outcome outcome;
+ int errnoValue;
+};
+
+AcquireResult acquire(const std::string& path, Memory& memory);
+
+struct ReleaseResult {
+ enum Outcome {
+ Success,
+ MunmapFailed,
+ };
+ Outcome outcome;
+ int errnoValue;
+};
+
+ReleaseResult release(Memory memory);
+
+class PipelineCacheStore {
+public:
+ PipelineCacheStore(useconds_t writeThrottleInterval);
+
+ ~PipelineCacheStore();
+
+ PipelineCacheStore(const PipelineCacheStore&) = delete;
+ PipelineCacheStore& operator=(const PipelineCacheStore&) = delete;
+
+ // Address must be stable as data is accessed in background thread
+ PipelineCacheStore(PipelineCacheStore&&) = delete;
+ PipelineCacheStore& operator=(PipelineCacheStore&&) = delete;
+
+ void store(std::string path, std::vector<uint8_t> data);
+
+private:
+ void runThread();
+
+ useconds_t mWriteThrottleInterval;
+
+ std::mutex mMutex;
+ std::condition_variable mConditionVariable;
+
+ struct StoreRequest {
+ std::string path;
+ std::vector<uint8_t> data;
+ };
+ std::optional<StoreRequest> mStoreRequest;
+
+ std::atomic_bool mExit;
+ std::thread mThread;
+};
+
+class PipelineCache {
+public:
+ PipelineCache(std::string storePath, useconds_t writeThrottleInterval);
+
+ sk_sp<SkData> tryLoad(const SkData& key);
+ bool canStore(const SkString& description) const;
+ void store(const SkData& key, const SkData& data);
+
+private:
+ std::string mStorePath;
+ PipelineCacheStore mPipelineCacheStore;
+
+ sk_sp<SkData> mKey;
+ sk_sp<SkData> mData;
+};
diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp
index 2771780..0c2dfd0 100644
--- a/libs/hwui/renderthread/CacheManager.cpp
+++ b/libs/hwui/renderthread/CacheManager.cpp
@@ -32,6 +32,7 @@
#include "RenderThread.h"
#include "VulkanManager.h"
#include "pipeline/skia/ATraceMemoryDump.h"
+#include "pipeline/skia/PersistentGraphicsCache.h"
#include "pipeline/skia/ShaderCache.h"
#include "pipeline/skia/SkiaMemoryTracer.h"
#include "renderstate/RenderState.h"
@@ -106,9 +107,11 @@
contextOptions->fGlyphCacheTextureMaximumBytes = mMaxGpuFontAtlasBytes;
contextOptions->fExecutor = &sDefaultExecutor;
- auto& cache = skiapipeline::ShaderCache::get();
- cache.initShaderDiskCache(identity, size);
- contextOptions->fPersistentCache = &cache;
+ auto& shaderCache = skiapipeline::ShaderCache::get();
+ shaderCache.initShaderDiskCache(identity, size);
+
+ auto& graphicsCache = skiapipeline::PersistentGraphicsCache::get();
+ contextOptions->fPersistentCache = &graphicsCache;
}
static GrPurgeResourceOptions toSkiaEnum(bool scratchOnly) {
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index a2ef4c6..4ea5a4e 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -36,7 +36,7 @@
#include "Properties.h"
#include "RenderThread.h"
-#include "pipeline/skia/ShaderCache.h"
+#include "pipeline/skia/PersistentGraphicsCache.h"
#include "renderstate/RenderState.h"
namespace android {
@@ -44,7 +44,7 @@
namespace renderthread {
// Not all of these are strictly required, but are all enabled if present.
-static std::array<std::string_view, 25> sEnableExtensions{
+static std::array<std::string_view, 26> sEnableExtensions{
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -70,6 +70,7 @@
VK_EXT_DEVICE_FAULT_EXTENSION_NAME,
VK_EXT_FRAME_BOUNDARY_EXTENSION_NAME,
VK_ANDROID_FRAME_BOUNDARY_EXTENSION_NAME,
+ VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME,
};
static bool shouldEnableExtension(const std::string_view& extension) {
@@ -341,6 +342,16 @@
tailPNext = &formatFeatures->pNext;
}
+ if (grExtensions.hasExtension(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME, 1)) {
+ VkPhysicalDevicePipelineCreationCacheControlFeatures* cacheControlFeatures =
+ new VkPhysicalDevicePipelineCreationCacheControlFeatures;
+ cacheControlFeatures->sType =
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES;
+ cacheControlFeatures->pNext = nullptr;
+ *tailPNext = cacheControlFeatures;
+ tailPNext = &cacheControlFeatures->pNext;
+ }
+
VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT* globalPriorityQueryFeatures =
new VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT;
globalPriorityQueryFeatures->sType =
@@ -793,7 +804,7 @@
mQueueWaitIdle(mGraphicsQueue);
}
- skiapipeline::ShaderCache::get().onVkFrameFlushed(context);
+ skiapipeline::PersistentGraphicsCache::get().onVkFrameFlushed(context);
return drawResult;
}
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 8ab2b16..6476576 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -22,27 +22,52 @@
#include <Properties.h>
#include <Rect.h>
#include <RenderNode.h>
-#include <hwui/Bitmap.h>
-#include <pipeline/skia/SkiaRecordingCanvas.h>
-#include <private/hwui/DrawGlInfo.h>
-#include <renderstate/RenderState.h>
-#include <renderthread/RenderThread.h>
-
#include <SkBitmap.h>
#include <SkColor.h>
#include <SkFont.h>
#include <SkImageInfo.h>
#include <SkRefCnt.h>
-
+#include <android-base/expected.h>
+#include <errno.h>
+#include <fcntl.h>
#include <gtest/gtest.h>
+#include <hwui/Bitmap.h>
+#include <pipeline/skia/PersistentGraphicsCache.h>
+#include <pipeline/skia/SkiaRecordingCanvas.h>
+#include <poll.h>
+#include <private/hwui/DrawGlInfo.h>
+#include <renderstate/RenderState.h>
+#include <renderthread/RenderThread.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <unistd.h>
+
+#include <array>
+#include <fstream>
#include <memory>
+#include <queue>
+#include <string>
+#include <string_view>
#include <unordered_map>
+#include <utility>
class SkCanvas;
class SkMatrix;
class SkPath;
struct SkRect;
+#ifdef __linux__
+#include <com_android_graphics_hwui_flags.h>
+namespace hwui_flags = com::android::graphics::hwui::flags;
+#else // __linux__
+namespace hwui_flags {
+constexpr bool separate_pipeline_cache() {
+ return false;
+}
+} // namespace hwui_flags
+#endif // __linux__
+
namespace android {
namespace uirenderer {
@@ -64,6 +89,10 @@
#define INNER_PIPELINE_RENDERTHREAD_TEST(test_case_name, test_name) \
TEST(test_case_name, test_name) { \
+ if (hwui_flags::separate_pipeline_cache()) { \
+ android::uirenderer::skiapipeline::PersistentGraphicsCache::get().initPipelineCache( \
+ "pipeline_cache.bin"); \
+ } \
TestUtils::runOnRenderThread(test_case_name##_##test_name##_RenderThreadTest::doTheThing); \
}
@@ -101,6 +130,151 @@
T mOldValue;
};
+/**
+ * Monitors a file for write events, allowing tests to detect whether a file has been written to or
+ * not within an expected timeframe.
+ */
+class FileEventMonitor {
+private:
+ static constexpr std::array kExpectedEvents{IN_MODIFY, IN_CLOSE_WRITE};
+
+ int mFd;
+
+public:
+ struct CreateResult {
+ enum Outcome {
+ Success,
+ InitFailed,
+ AddWatchFailed,
+ };
+ Outcome outcome;
+ int errnoValue;
+ std::unique_ptr<FileEventMonitor> monitor;
+ };
+
+ static CreateResult create(const std::string& path) {
+ auto fd = inotify_init();
+ if (fd == -1) {
+ return CreateResult{.outcome = CreateResult::InitFailed, .errnoValue = errno};
+ }
+
+ uint32_t eventMask = 0;
+ for (auto event : kExpectedEvents) {
+ eventMask |= event;
+ }
+ auto wd = inotify_add_watch(fd, path.c_str(), eventMask);
+ if (wd == -1) {
+ return CreateResult{.outcome = CreateResult::AddWatchFailed, .errnoValue = errno};
+ }
+
+ return CreateResult{
+ .outcome = CreateResult::Success,
+ .monitor = std::make_unique<FileEventMonitor>(fd),
+ };
+ }
+
+ FileEventMonitor(int fd) : mFd(fd) {}
+ ~FileEventMonitor() { close(mFd); }
+
+ FileEventMonitor(const FileEventMonitor&) = delete;
+ FileEventMonitor(FileEventMonitor&&) = delete;
+
+ FileEventMonitor& operator=(const FileEventMonitor&) = delete;
+ FileEventMonitor& operator=(FileEventMonitor&&) = delete;
+
+ enum class AwaitResult {
+ Success,
+ PollError,
+ TimedOut,
+ NotEnoughData,
+ };
+ // Note that the timeout is not a total runtime timeout but rather the timeout for each file
+ // system event, therefore the total time spent waiting may be a multiple of the timeout.
+ AwaitResult awaitWriteOrTimeout(int timeoutMs = 100) {
+ std::queue<uint32_t> expectedEvents;
+ for (auto event : kExpectedEvents) {
+ expectedEvents.push(event);
+ }
+
+ while (!expectedEvents.empty()) {
+ pollfd fd = {
+ .fd = mFd,
+ .events = POLLIN,
+ .revents = 0,
+ };
+ auto result = poll(&fd, 1, timeoutMs);
+ if (result == -1) {
+ return AwaitResult::PollError;
+ }
+ if (result == 0) {
+ return AwaitResult::TimedOut;
+ }
+
+ inotify_event event;
+ auto bytes = read(mFd, &event, sizeof(event));
+ if (bytes != sizeof(event)) {
+ return AwaitResult::NotEnoughData;
+ }
+
+ if (event.mask & expectedEvents.front()) {
+ expectedEvents.pop();
+ }
+ }
+ return AwaitResult::Success;
+ }
+};
+
+#define ASSERT_SUCCESS(RESULT) \
+ ASSERT_EQ(FileEventMonitor::CreateResult::Success, (RESULT).outcome) \
+ << "errno=" << (RESULT).errnoValue << " (" << strerror((RESULT).errnoValue) << ")";
+
+/**
+ * Allows tests to create a temporary file for use in the test, which is automatically removed once
+ * the test exits.
+ */
+class TestFile {
+private:
+ std::string mPath;
+
+ static std::string getTempPath(std::string filename) { return "/data/local/tmp/" + filename; }
+
+public:
+ static android::base::expected<TestFile, int> ensureExistsEmpty(std::string filename) {
+ auto path = getTempPath(std::move(filename));
+ auto fd = creat(path.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
+ if (fd == -1) {
+ return android::base::unexpected(errno);
+ }
+ close(fd);
+ return android::base::expected<TestFile, int>(std::in_place, std::move(path));
+ }
+
+ static android::base::expected<TestFile, int> ensureDoesNotExist(std::string filename) {
+ auto path = getTempPath(std::move(filename));
+ auto result = remove(path.c_str());
+ if ((result == -1) && (errno != ENOENT)) {
+ return android::base::unexpected(errno);
+ }
+ return android::base::expected<TestFile, int>(std::in_place, std::move(path));
+ }
+
+ TestFile(std::string path) : mPath(std::move(path)) {}
+ ~TestFile() { remove(mPath.c_str()); }
+
+ TestFile(const TestFile&) = delete;
+ TestFile(TestFile&&) = delete;
+
+ TestFile& operator=(const TestFile&) = delete;
+ TestFile& operator=(TestFile&&) = delete;
+
+ const std::string& path() const { return mPath; }
+
+ void write(std::string_view contents) {
+ std::ofstream stream(mPath);
+ stream << contents;
+ }
+};
+
class TestUtils {
public:
class SignalingDtor {
diff --git a/libs/hwui/tests/unit/PersistentGraphicsCacheTests.cpp b/libs/hwui/tests/unit/PersistentGraphicsCacheTests.cpp
new file mode 100644
index 0000000..4c5fe64
--- /dev/null
+++ b/libs/hwui/tests/unit/PersistentGraphicsCacheTests.cpp
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <SkData.h>
+#include <SkRefCnt.h>
+#include <SkString.h>
+#include <ganesh/GrDirectContext.h>
+#include <gtest/gtest.h>
+#include <sys/types.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string>
+
+#include "Properties.h"
+#include "pipeline/skia/PersistentGraphicsCache.h"
+#include "tests/common/TestUtils.h"
+
+// RENDERTHREAD_TEST declares both SkiaVK and SkiaGL variants.
+#define VK_ONLY() \
+ if (Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan) { \
+ GTEST_SKIP() << "This test is only applicable to RenderPipelineType::SkiaVulkan"; \
+ }
+
+#define ENSURE_FLAG_ENABLED() \
+ if (!hwui_flags::separate_pipeline_cache()) { \
+ GTEST_SKIP() << "This test is only applicable when the separate_pipeline_cache aconfig " \
+ "flag is enabled"; \
+ }
+
+using namespace android::uirenderer;
+using namespace android::uirenderer::skiapipeline;
+
+namespace {
+
+constexpr char kFilename[] = "pipeline_cache.bin";
+
+template <typename T>
+sk_sp<SkData> createData(const T value) {
+ return SkData::MakeWithCopy(&value, sizeof(value));
+}
+
+// Hardcoded Skia enum value - tests may break if Skia changes the key.
+sk_sp<SkData> getPipelineCacheKey() {
+ static sk_sp<SkData> keyData = createData<uint32_t>(1);
+ return keyData;
+}
+
+} // namespace
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+class PersistentGraphicsCacheTestUtils {
+private:
+ class MockGrDirectContextWrapper : public PersistentGraphicsCache::GrDirectContextWrapper {
+ private:
+ bool mCanDetectNewVkPipelineCacheData;
+ bool mHasNewVkPipelineCacheData;
+ GrDirectContext* mRealContext;
+
+ public:
+ MockGrDirectContextWrapper(bool canDetectNewVkPipelineCacheData,
+ bool hasNewVkPipelineCacheData, GrDirectContext* realContext)
+ : mCanDetectNewVkPipelineCacheData(canDetectNewVkPipelineCacheData)
+ , mHasNewVkPipelineCacheData(hasNewVkPipelineCacheData)
+ , mRealContext(realContext) {}
+
+ bool canDetectNewVkPipelineCacheData() const override {
+ return mCanDetectNewVkPipelineCacheData;
+ }
+
+ bool hasNewVkPipelineCacheData() const override { return mHasNewVkPipelineCacheData; }
+
+ void storeVkPipelineCacheData(size_t maxSize) override {
+ mRealContext->storeVkPipelineCacheData(maxSize);
+ }
+
+ GrDirectContext* unwrap() const override { return mRealContext; }
+ };
+
+ static void reset(PersistentGraphicsCache& cache) {
+ cache.~PersistentGraphicsCache();
+ new (&cache) PersistentGraphicsCache();
+ }
+
+public:
+ static PersistentGraphicsCache& newCache(const std::string& path,
+ useconds_t writeThrottleInterval = 0) {
+ auto& cache = PersistentGraphicsCache::get();
+ reset(cache);
+ cache.initPipelineCache(path, writeThrottleInterval);
+ return cache;
+ }
+
+ static void onVkFrameFlushed(PersistentGraphicsCache& cache,
+ bool canDetectNewVkPipelineCacheData,
+ bool hasNewVkPipelineCacheData, GrDirectContext* realContext) {
+ MockGrDirectContextWrapper wrapper(canDetectNewVkPipelineCacheData,
+ hasNewVkPipelineCacheData, realContext);
+ cache.onVkFrameFlushed(&wrapper);
+ }
+};
+
+} // namespace skiapipeline
+} // namespace uirenderer
+} // namespace android
+
+TEST(PersistentGraphicsCacheTest, emptyFile_loadKey_isEmptyByDefault) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ // Act
+ auto result = cache.load(*getPipelineCacheKey());
+
+ // Assert
+ ASSERT_EQ(nullptr, result);
+}
+
+TEST(PersistentGraphicsCacheTest, store_load_returnsIdenticalData) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+
+ uint64_t dataValue = 5;
+ auto key = createData<uint32_t>(10);
+ auto data = createData(dataValue);
+
+ // Act
+ cache.store(*key, *data, SkString("VkPipelineCache"));
+ ASSERT_EQ(FileEventMonitor::AwaitResult::Success,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+ auto result = PersistentGraphicsCacheTestUtils::newCache(file->path()).load(*key);
+
+ // Assert
+ ASSERT_NE(nullptr, result);
+ ASSERT_EQ(sizeof(dataValue), result->size());
+ ASSERT_EQ(0, memcmp(&dataValue, result->data(), sizeof(dataValue)));
+}
+
+RENDERTHREAD_TEST(PersistentGraphicsCacheTest, hasPipelineCreationCacheControl_newCache_isStored) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+ VK_ONLY();
+
+ auto context = renderThread.getGrContext();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+
+ // Act
+ PersistentGraphicsCacheTestUtils::onVkFrameFlushed(cache,
+ /* canDetectNewVkPipelineCacheData= */ true,
+ /* hasNewPipelineCache= */ true, context);
+
+ // Assert
+ ASSERT_EQ(FileEventMonitor::AwaitResult::Success,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+ auto result =
+ PersistentGraphicsCacheTestUtils::newCache(file->path()).load(*getPipelineCacheKey());
+ ASSERT_NE(nullptr, result);
+ ASSERT_NE(nullptr, result->data());
+ ASSERT_GT(result->size(), 0);
+}
+
+RENDERTHREAD_TEST(PersistentGraphicsCacheTest,
+ hasPipelineCreationCacheControl_oldCache_isNotStored) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+ VK_ONLY();
+
+ auto context = renderThread.getGrContext();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+
+ // Act
+ PersistentGraphicsCacheTestUtils::onVkFrameFlushed(cache,
+ /* canDetectNewVkPipelineCacheData= */ true,
+ /* hasNewPipelineCache= */ false, context);
+
+ // Assert
+ ASSERT_EQ(FileEventMonitor::AwaitResult::TimedOut,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+ auto result =
+ PersistentGraphicsCacheTestUtils::newCache(file->path()).load(*getPipelineCacheKey());
+ ASSERT_EQ(nullptr, result);
+}
+
+RENDERTHREAD_TEST(PersistentGraphicsCacheTest,
+ noPipelineCreationCacheControl_newCacheBySize_isStored) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+ VK_ONLY();
+
+ auto context = renderThread.getGrContext();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+
+ // Act
+ // Current cache size is 0, so cache is new by size
+ PersistentGraphicsCacheTestUtils::onVkFrameFlushed(cache,
+ /* canDetectNewVkPipelineCacheData= */ false,
+ /* hasNewPipelineCache= */ true, context);
+
+ // Assert
+ ASSERT_EQ(FileEventMonitor::AwaitResult::Success,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+ auto result =
+ PersistentGraphicsCacheTestUtils::newCache(file->path()).load(*getPipelineCacheKey());
+ ASSERT_NE(nullptr, result);
+ ASSERT_NE(nullptr, result->data());
+ ASSERT_GT(result->size(), 0);
+}
+
+RENDERTHREAD_TEST(PersistentGraphicsCacheTest,
+ noPipelineCreationCacheControl_oldCacheBySize_isNotStored) {
+ // Arrange
+ ENSURE_FLAG_ENABLED();
+ VK_ONLY();
+
+ auto context = renderThread.getGrContext();
+
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ auto& cache = PersistentGraphicsCacheTestUtils::newCache(file->path());
+
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+
+ // Current cache size is 0, so cache is new by size
+ PersistentGraphicsCacheTestUtils::onVkFrameFlushed(cache,
+ /* canDetectNewVkPipelineCacheData= */ false,
+ /* hasNewPipelineCache= */ true, context);
+ ASSERT_EQ(FileEventMonitor::AwaitResult::Success,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+
+ // Act
+ // Cache size has not changed, so cache is old by size
+ PersistentGraphicsCacheTestUtils::onVkFrameFlushed(cache,
+ /* canDetectNewVkPipelineCacheData= */ false,
+ /* hasNewPipelineCache= */ true, context);
+
+ // Assert
+ ASSERT_EQ(FileEventMonitor::AwaitResult::TimedOut,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+}
diff --git a/libs/hwui/tests/unit/PipelineCacheTests.cpp b/libs/hwui/tests/unit/PipelineCacheTests.cpp
new file mode 100644
index 0000000..d60867b
--- /dev/null
+++ b/libs/hwui/tests/unit/PipelineCacheTests.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android-base/scopeguard.h>
+#include <gtest/gtest.h>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include "pipeline/skia/PipelineCache.h"
+#include "tests/common/TestUtils.h"
+
+using namespace android::uirenderer;
+
+namespace {
+
+constexpr char kFilename[] = "blobcache.bin";
+
+}
+
+TEST(PipelineCacheTest, noFile_acquire_fails) {
+ // Arrange
+ auto file = TestFile::ensureDoesNotExist(kFilename);
+ ASSERT_TRUE(file.has_value());
+
+ // Act
+ Memory mem;
+ auto result = acquire(file->path(), mem);
+
+ // Assert
+ ASSERT_EQ(AcquireResult::OpenFailed, result.outcome);
+}
+
+TEST(PipelineCacheTest, existingFile_acquire_succeeds) {
+ // Arrange
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+ file->write("data");
+
+ // Act
+ Memory mem;
+ auto result = acquire(file->path(), mem);
+ auto _ = android::base::make_scope_guard([=]() { release(mem); });
+
+ // Assert
+ ASSERT_EQ(AcquireResult::Success, result.outcome);
+ ASSERT_EQ(std::string_view("data"),
+ std::string_view(static_cast<const char*>(mem.data), mem.size));
+}
+
+TEST(PipelineCacheTest, existingFile_acquireAndRelease_succeeds) {
+ // Arrange
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+ file->write("data");
+
+ // Act
+ Memory mem;
+ auto acquireResult = acquire(file->path(), mem);
+ auto releaseResult = release(mem);
+
+ // Assert
+ ASSERT_EQ(AcquireResult::Success, acquireResult.outcome);
+ ASSERT_EQ(ReleaseResult::Success, releaseResult.outcome);
+}
+
+TEST(PipelineCacheTest, existingFile_backgroundWrite_overwritesData) {
+ // Arrange
+ auto file = TestFile::ensureExistsEmpty(kFilename);
+ ASSERT_TRUE(file.has_value());
+ file->write("data");
+ auto monitorCreateResult = FileEventMonitor::create(file->path());
+ ASSERT_SUCCESS(monitorCreateResult);
+ PipelineCacheStore cache(0);
+
+ // Act
+ cache.store(file->path(), std::vector<uint8_t>({'b', 'l', 'o', 'b'}));
+
+ // Assert
+ ASSERT_EQ(FileEventMonitor::AwaitResult::Success,
+ monitorCreateResult.monitor->awaitWriteOrTimeout());
+ Memory mem;
+ auto result = acquire(file->path(), mem);
+ auto _ = android::base::make_scope_guard([=]() { release(mem); });
+ ASSERT_EQ(AcquireResult::Success, result.outcome);
+ ASSERT_EQ(std::string_view("blob"),
+ std::string_view(static_cast<const char*>(mem.data), mem.size));
+}
diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp
index b714534..fba11e6 100644
--- a/libs/hwui/tests/unit/ShaderCacheTests.cpp
+++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp
@@ -371,6 +371,10 @@
using namespace android::uirenderer;
RENDERTHREAD_TEST(ShaderCacheTest, testOnVkFrameFlushed) {
+ if (hwui_flags::separate_pipeline_cache()) {
+ GTEST_SKIP() << "This test is only applicable when the separate_pipeline_cache aconfig "
+ "flag is disabled";
+ }
if (Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan) {
// RENDERTHREAD_TEST declares both SkiaVK and SkiaGL variants.
GTEST_SKIP() << "This test is only applicable to RenderPipelineType::SkiaVulkan";