From e7c96cd12499b3daba4d3927c73a90637908c43f Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Tue, 23 Jul 2024 19:04:47 -0700 Subject: [PATCH] better handle skipped frames and cache eviction Add Renderer::skipFrame() which should be called when intentionally skipping frames, for instance because the screen content hasn't changed, allowing Filament to performance needed periodic tasks, such as cache garbage collection and callback dispatches. We also improve the ResourceAllocator cache eviction policy: - the cache is aggressively purged when skipping a frame - we aggressively evict entries older than 3 frames The default Config is now set to a more agressive setting. --- .../src/main/cpp/Renderer.cpp | 8 +++ .../com/google/android/filament/Engine.java | 7 +- .../com/google/android/filament/Renderer.java | 15 ++++ filament/include/filament/Engine.h | 7 +- filament/include/filament/Renderer.h | 8 +++ filament/src/Renderer.cpp | 4 ++ filament/src/ResourceAllocator.cpp | 72 +++++++++++++++---- filament/src/ResourceAllocator.h | 3 +- filament/src/details/Renderer.cpp | 33 +++++++++ filament/src/details/Renderer.h | 3 + web/filament-js/jsbindings.cpp | 1 + 11 files changed, 143 insertions(+), 18 deletions(-) diff --git a/android/filament-android/src/main/cpp/Renderer.cpp b/android/filament-android/src/main/cpp/Renderer.cpp index 1dc252d356..0020ea21d6 100644 --- a/android/filament-android/src/main/cpp/Renderer.cpp +++ b/android/filament-android/src/main/cpp/Renderer.cpp @@ -28,6 +28,14 @@ using namespace filament; using namespace backend; + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_Renderer_nSkipFrame(JNIEnv *, jclass, jlong nativeRenderer, + jlong vsyncSteadyClockTimeNano) { + Renderer *renderer = (Renderer *) nativeRenderer; + renderer->skipFrame(uint64_t(vsyncSteadyClockTimeNano)); +} + extern "C" JNIEXPORT jboolean JNICALL Java_com_google_android_filament_Renderer_nBeginFrame(JNIEnv *, jclass, jlong nativeRenderer, jlong nativeSwapChain, jlong frameTimeNanos) { diff --git a/android/filament-android/src/main/java/com/google/android/filament/Engine.java b/android/filament-android/src/main/java/com/google/android/filament/Engine.java index 6c4c5119e3..6cadf7d5c7 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Engine.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Engine.java @@ -425,9 +425,12 @@ public class Engine { public long resourceAllocatorCacheSizeMB = 64; /* - * This value determines for how many frames are texture entries kept in the cache. + * This value determines how many frames texture entries are kept for in the cache. This + * is a soft limit, meaning some texture older than this are allowed to stay in the cache. + * Typically only one texture is evicted per frame. + * The default is 1. */ - public long resourceAllocatorCacheMaxAge = 2; + public long resourceAllocatorCacheMaxAge = 1; /* * Disable backend handles use-after-free checks. diff --git a/android/filament-android/src/main/java/com/google/android/filament/Renderer.java b/android/filament-android/src/main/java/com/google/android/filament/Renderer.java index 4fb447db2c..509a3c080c 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Renderer.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Renderer.java @@ -297,6 +297,20 @@ public class Renderer { nSetVsyncTime(getNativeObject(), steadyClockTimeNano); } + /** + * Call skipFrame when momentarily skipping frames, for instance if the content of the + * scene doesn't change. + * + * @param vsyncSteadyClockTimeNano The time in nanoseconds when the frame started being rendered, + * in the {@link System#nanoTime()} timebase. Divide this value by 1000000 to + * convert it to the {@link android.os.SystemClock#uptimeMillis()} + * time base. This typically comes from + * {@link android.view.Choreographer.FrameCallback}. + */ + public void skipFrame(long vsyncSteadyClockTimeNano) { + nSkipFrame(getNativeObject(), vsyncSteadyClockTimeNano); + } + /** * Sets up a frame for this Renderer. *

beginFrame manages frame pacing, and returns whether or not a frame should be @@ -716,6 +730,7 @@ public class Renderer { private static native void nSetPresentationTime(long nativeObject, long monotonicClockNanos); private static native void nSetVsyncTime(long nativeObject, long steadyClockTimeNano); + private static native void nSkipFrame(long nativeObject, long vsyncSteadyClockTimeNano); private static native boolean nBeginFrame(long nativeRenderer, long nativeSwapChain, long frameTimeNanos); private static native void nEndFrame(long nativeRenderer); private static native void nRender(long nativeRenderer, long nativeView); diff --git a/filament/include/filament/Engine.h b/filament/include/filament/Engine.h index 6c3d87c29c..f728e0dde3 100644 --- a/filament/include/filament/Engine.h +++ b/filament/include/filament/Engine.h @@ -350,9 +350,12 @@ public: uint32_t resourceAllocatorCacheSizeMB = 64; /* - * This value determines for how many frames are texture entries kept in the cache. + * This value determines how many frames texture entries are kept for in the cache. This + * is a soft limit, meaning some texture older than this are allowed to stay in the cache. + * Typically only one texture is evicted per frame. + * The default is 1. */ - uint32_t resourceAllocatorCacheMaxAge = 2; + uint32_t resourceAllocatorCacheMaxAge = 1; /* * Disable backend handles use-after-free checks. diff --git a/filament/include/filament/Renderer.h b/filament/include/filament/Renderer.h index 9bce76867e..a4e919610d 100644 --- a/filament/include/filament/Renderer.h +++ b/filament/include/filament/Renderer.h @@ -271,6 +271,14 @@ public: */ void setVsyncTime(uint64_t steadyClockTimeNano) noexcept; + /** + * Call skipFrame when momentarily skipping frames, for instance if the content of the + * scene doesn't change. + * + * @param vsyncSteadyClockTimeNano + */ + void skipFrame(uint64_t vsyncSteadyClockTimeNano = 0u); + /** * Set-up a frame for this Renderer. * diff --git a/filament/src/Renderer.cpp b/filament/src/Renderer.cpp index 01546a325b..258fd5ed32 100644 --- a/filament/src/Renderer.cpp +++ b/filament/src/Renderer.cpp @@ -45,6 +45,10 @@ void Renderer::setPresentationTime(int64_t monotonic_clock_ns) { downcast(this)->setPresentationTime(monotonic_clock_ns); } +void Renderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) { + downcast(this)->skipFrame(vsyncSteadyClockTimeNano); +} + bool Renderer::beginFrame(SwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) { return downcast(this)->beginFrame(downcast(swapChain), vsyncSteadyClockTimeNano); } diff --git a/filament/src/ResourceAllocator.cpp b/filament/src/ResourceAllocator.cpp index 3f92acf784..471f8778bd 100644 --- a/filament/src/ResourceAllocator.cpp +++ b/filament/src/ResourceAllocator.cpp @@ -27,6 +27,8 @@ #include "private/backend/DriverApi.h" +#include +#include #include #include #include @@ -209,6 +211,7 @@ void ResourceAllocator::destroyTexture(TextureHandle h) noexcept { uint32_t const size = key.value().getSize(); mTextureCache.emplace(key.value(), TextureCachePayload{ h, mAge, size }); mCacheSize += size; + mCacheSizeHiWaterMark = std::max(mCacheSizeHiWaterMark, mCacheSize); } } else { mBackend.destroyTexture(h); @@ -219,40 +222,83 @@ ResourceAllocatorDisposerInterface& ResourceAllocator::getDisposer() noexcept { return *mDisposer; } -void ResourceAllocator::gc() noexcept { - // this is called regularly -- usually once per frame of each Renderer +void ResourceAllocator::gc(bool skippedFrame) noexcept { + // this is called regularly -- usually once per frame - // increase our age - const size_t age = mAge++; + // increase our age at each (non-skipped) frame + const size_t age = mAge; + if (!skippedFrame) { + mAge++; + } // Purging strategy: - // - remove entries that are older than a certain age - // - remove only one entry per gc(), + // - remove all entries older than MAX_AGE_SKIPPED_FRAME when skipping a frame + // - remove entries older than mCacheMaxAgeSoft + // - remove only MAX_EVICTION_COUNT entry per gc(), + // - look for the number of unique resource ages present in the cache (this basically gives + // us how many buckets of resources we have corresponding to previous frames. + // - remove all resources that have an age older than the MAX_UNIQUE_AGE_COUNT'th bucket auto& textureCache = mTextureCache; + + // when skipping a frame, the maximum age to keep in the cache + constexpr size_t MAX_AGE_SKIPPED_FRAME = 1; + + // maximum entry count to evict per GC, under the mCacheMaxAgeSoft limit + constexpr size_t MAX_EVICTION_COUNT = 1; + + // maximum number of unique ages in the cache + constexpr size_t MAX_UNIQUE_AGE_COUNT = 3; + + utils::bitset32 ages; + uint32_t evictedCount = 0; for (auto it = textureCache.begin(); it != textureCache.end();) { - const size_t ageDiff = age - it->second.age; - if (ageDiff >= mCacheMaxAge) { + size_t const ageDiff = age - it->second.age; + if ((ageDiff >= MAX_AGE_SKIPPED_FRAME && skippedFrame) || + (ageDiff >= mCacheMaxAge && evictedCount < MAX_EVICTION_COUNT)) { + evictedCount++; purge(it); - // only purge a single entry per gc - break; } else { + // build the set of ages present in the cache after eviction + ages.set(std::min(size_t(31), ageDiff)); ++it; } } + + // if we have MAX_UNIQUE_AGE_COUNT ages or more, we evict all the resources that + // are older than the MAX_UNIQUE_AGE_COUNT'th age. + if (!skippedFrame && ages.count() >= MAX_UNIQUE_AGE_COUNT) { + uint32_t bits = ages.getValue(); + // remove from the set the ages we keep + for (size_t i = 0; i < MAX_UNIQUE_AGE_COUNT - 1; i++) { + bits &= ~(1 << utils::ctz(bits)); + } + size_t const maxAge = utils::ctz(bits); + for (auto it = textureCache.begin(); it != textureCache.end();) { + const size_t ageDiff = age - it->second.age; + if (ageDiff >= maxAge) { + purge(it); + } else { + ++it; + } + } + } } UTILS_NOINLINE void ResourceAllocator::dump(bool brief) const noexcept { - slog.d << "# entries=" << mTextureCache.size() << ", sz=" << mCacheSize / float(1u << 20u) - << " MiB" << io::endl; + constexpr float MiB = 1.0f / float(1u << 20u); + slog.d << "# entries=" << mTextureCache.size() + << ", sz=" << (float)mCacheSize * MiB << " MiB" + << ", max=" << (float)mCacheSizeHiWaterMark * MiB << " MiB" + << io::endl; if (!brief) { for (auto const& it : mTextureCache) { auto w = it.first.width; auto h = it.first.height; auto f = FTexture::getFormatSize(it.first.format); slog.d << it.first.name << ": w=" << w << ", h=" << h << ", f=" << f << ", sz=" - << it.second.size / float(1u << 20u) << io::endl; + << (float)it.second.size * MiB << io::endl; } } } diff --git a/filament/src/ResourceAllocator.h b/filament/src/ResourceAllocator.h index 40f15f3b4a..0bcc9aefa2 100644 --- a/filament/src/ResourceAllocator.h +++ b/filament/src/ResourceAllocator.h @@ -113,7 +113,7 @@ public: ResourceAllocatorDisposerInterface& getDisposer() noexcept override; - void gc() noexcept; + void gc(bool skippedFrame = false) noexcept; private: size_t const mCacheMaxAge; @@ -222,6 +222,7 @@ private: CacheContainer mTextureCache; size_t mAge = 0; uint32_t mCacheSize = 0; + uint32_t mCacheSizeHiWaterMark = 0; static constexpr bool mEnabled = true; friend class ResourceAllocatorDisposer; diff --git a/filament/src/details/Renderer.cpp b/filament/src/details/Renderer.cpp index 9fd1758b82..a756c0cb9c 100644 --- a/filament/src/details/Renderer.cpp +++ b/filament/src/details/Renderer.cpp @@ -242,6 +242,39 @@ void FRenderer::setVsyncTime(uint64_t steadyClockTimeNano) noexcept { mVsyncSteadyClockTimeNano = steadyClockTimeNano; } +void FRenderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) { + SYSTRACE_CALL(); + + FILAMENT_CHECK_PRECONDITION(!mSwapChain) << + "skipFrame() can't be called between beginFrame() and endFrame()"; + + if (!vsyncSteadyClockTimeNano) { + vsyncSteadyClockTimeNano = mVsyncSteadyClockTimeNano; + mVsyncSteadyClockTimeNano = 0; + } + + FEngine& engine = mEngine; + FEngine::DriverApi& driver = engine.getDriverApi(); + + // Gives the backend a chance to execute periodic tasks. This must be called before + // the frame skipper. + driver.tick(); + + // do this before engine.flush() + mResourceAllocator->gc(true); + + // Run the component managers' GC in parallel + // WARNING: while doing this we can't access any component manager + auto& js = engine.getJobSystem(); + + auto *job = js.runAndRetain(jobs::createJob(js, nullptr, &FEngine::gc, &engine)); // gc all managers + + engine.flush(); // flush command stream + + // make sure we're done with the gcs + js.waitAndRelease(job); +} + bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) { assert_invariant(swapChain); diff --git a/filament/src/details/Renderer.h b/filament/src/details/Renderer.h index e204396434..c0c6d5aa1b 100644 --- a/filament/src/details/Renderer.h +++ b/filament/src/details/Renderer.h @@ -91,6 +91,9 @@ public: void setVsyncTime(uint64_t steadyClockTimeNano) noexcept; + // skip a frame + void skipFrame(uint64_t vsyncSteadyClockTimeNano); + // start a frame bool beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano); diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index e5ec3fad9b..7dcdb7c183 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -627,6 +627,7 @@ class_("Renderer") .function("getClearOptions", &Renderer::getClearOptions) .function("setPresentationTime", &Renderer::setPresentationTime) .function("setVsyncTime", &Renderer::setVsyncTime) + .function("skipFrame", &Renderer::skipFrame) .function("beginFrame", EMBIND_LAMBDA(bool, (Renderer* self, SwapChain* swapChain), { return self->beginFrame(swapChain); }), allow_raw_pointers())