diff --git a/android/filament-android/src/main/cpp/SwapChain.cpp b/android/filament-android/src/main/cpp/SwapChain.cpp index 8837b88c04..5f93b32507 100644 --- a/android/filament-android/src/main/cpp/SwapChain.cpp +++ b/android/filament-android/src/main/cpp/SwapChain.cpp @@ -28,9 +28,8 @@ Java_com_google_android_filament_SwapChain_nSetFrameCompletedCallback(JNIEnv* en jlong nativeSwapChain, jobject handler, jobject runnable) { SwapChain* swapChain = (SwapChain*) nativeSwapChain; auto* callback = JniCallback::make(env, handler, runnable); - swapChain->setFrameCompletedCallback(nullptr, [callback](SwapChain* swapChain) { - JniCallback::postToJavaAndDestroy(callback); - }); + swapChain->setFrameCompletedCallback(callback->getHandler(), + [callback](SwapChain* swapChain) { JniCallback::postToJavaAndDestroy(callback); }); } extern "C" JNIEXPORT jboolean JNICALL @@ -53,3 +52,22 @@ Java_com_google_android_filament_SwapChain_nIsProtectedContentSupported( Engine* engine = (Engine*) nativeEngine; return (jboolean)SwapChain::isProtectedContentSupported(*engine); } + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_SwapChain_nSetFrameScheduledCallback(JNIEnv* env, jclass, + jlong nativeSwapChain, jobject handler, jobject runnable) { + SwapChain* swapChain = (SwapChain*) nativeSwapChain; + auto* callback = JniCallback::make(env, handler, runnable); + swapChain->setFrameScheduledCallback(callback->getHandler(), + [callback](backend::PresentCallable) { + // Ignore PresentCallable, which is only meaningful with the Metal backend. + JniCallback::postToJavaAndDestroy(callback); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_google_android_filament_SwapChain_nIsFrameScheduledCallbackSet( + JNIEnv *, jclass, jlong nativeSwapChain) { + SwapChain* swapChain = (SwapChain*) nativeSwapChain; + return (jboolean)swapChain->isFrameScheduledCallbackSet(); +} diff --git a/android/filament-android/src/main/java/com/google/android/filament/SwapChain.java b/android/filament-android/src/main/java/com/google/android/filament/SwapChain.java index cf15621aeb..f7aed9331d 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/SwapChain.java +++ b/android/filament-android/src/main/java/com/google/android/filament/SwapChain.java @@ -139,6 +139,78 @@ public class SwapChain { nSetFrameCompletedCallback(getNativeObject(), handler, callback); } + /** + * FrameScheduledCallback is a callback function that notifies an application about the status + * of a frame after Filament has finished its processing. + * + *
+ * The exact timing and semantics of this callback differ depending on the graphics backend in + * use. + *
+ * + *+ * With the Metal backend, this callback signifies that Filament has completed all CPU-side + * processing for a frame and the frame is ready to be scheduled for presentation. + *
+ * + *+ * Typically, Filament is responsible for scheduling the frame's presentation to the SwapChain. + * If a FrameScheduledCallback is set, however, the application bears the responsibility of + * scheduling the frame for presentation by calling the PresentCallable passed to the callback + * function. In this mode, Filament will not automatically schedule the frame for presentation. + *
+ * + *+ * When using the Metal backend, if your application delays the call to the PresentCallable + * (e.g., by invoking it on a separate thread), you must ensure all PresentCallables have been + * called before shutting down the Filament Engine. You can guarantee this by calling + * Engine.flushAndWait() before Engine.shutdown(). This is necessary to ensure the Engine has + * a chance to clean up all memory related to frame presentation. + *
+ * + *+ * On other backends, this callback serves as a notification that Filament has completed all + * CPU-side processing for a frame. Filament proceeds with its normal presentation logic + * automatically, and the PresentCallable passed to the callback is a no-op that can be safely + * ignored. + *
+ * + *+ * A FrameScheduledCallback can be set on an individual SwapChain through + * setFrameScheduledCallback. Each SwapChain can have only one callback set per frame. If + * setFrameScheduledCallback is called multiple times on the same SwapChain before + * Renderer.endFrame(), the most recent call effectively overwrites any previously set callback. + *
+ * + *+ * The callback set by setFrameScheduledCallback is "latched" when Renderer.endFrame() is + * executed. At this point, the callback is fixed for the frame that was just encoded. + * Subsequent calls to setFrameScheduledCallback after endFrame() will apply to the next frame. + *
+ * + *+ * Use setFrameScheduledCallback() (with default arguments) to unset the callback. + *
+ * + * @param handler A {@link java.util.concurrent.Executor Executor}. + * @param callback The Runnable callback to invoke when frame processing is complete. + */ + public void setFrameScheduledCallback(@NonNull Object handler, @NonNull Runnable callback) { + nSetFrameScheduledCallback(getNativeObject(), handler, callback); + } + + /** + * Returns whether this SwapChain currently has a FrameScheduledCallback set. + * + * @return true, if the last call to setFrameScheduledCallback set a callback + */ + public boolean isFrameScheduledCallbackSet() { + return nIsFrameScheduledCallbackSet(getNativeObject()); + } + public long getNativeObject() { if (mNativeObject == 0) { throw new IllegalStateException("Calling method on destroyed SwapChain"); @@ -151,6 +223,8 @@ public class SwapChain { } private static native void nSetFrameCompletedCallback(long nativeSwapChain, Object handler, Runnable callback); + private static native void nSetFrameScheduledCallback(long nativeSwapChain, Object handler, Runnable callback); + private static native boolean nIsFrameScheduledCallbackSet(long nativeSwapChain); private static native boolean nIsSRGBSwapChainSupported(long nativeEngine); private static native boolean nIsMSAASwapChainSupported(long nativeEngine, int samples); private static native boolean nIsProtectedContentSupported(long nativeEngine);