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())