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